<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>benn博客</title><link>https://bennhuang.com/zh/</link><description>Recent content on benn博客</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><atom:link href="https://bennhuang.com/zh/index.xml" rel="self" type="application/rss+xml"/><item><title>为什么你的 OpenClaw 上网总是遇到验证码</title><link>https://bennhuang.com/zh/posts/why-your-openclaws-browser-keeps-getting-captchas/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://bennhuang.com/zh/posts/why-your-openclaws-browser-keeps-getting-captchas/</guid><description>
&lt;p>我日常使用 OpenClaw，前段时间遇到一个问题：深度研究任务的结果总是很差。&lt;/p>
&lt;p>排查之后发现，原因是浏览器被各大网站的反机器人系统拦截了。Agent 访问不了正常的信息源，只好退而求其次，去找一些质量差的替代来源，甚至直接编造内容。而这一切是静默发生的，表面上看不出任何异常。&lt;/p>
&lt;p>本文记录一下这个问题的原因和解决方案。&lt;/p>
&lt;h2 id="一被拦截">
&lt;a href="#%e4%b8%80%e8%a2%ab%e6%8b%a6%e6%88%aa" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
一、被拦截
&lt;/h2>
&lt;p>OpenClaw 内置的浏览器基于 Chromium 和 Playwright。我的环境是一台 MacBook Pro，家庭网络，浏览器里登录了各种账号，看上去和正常用户没有区别。&lt;/p>
&lt;p>但实际情况是，几乎所有有反机器人检测的网站都会拦截它。Google 和 Bing 弹出验证码，X（原 Twitter）弹出登录墙，Medium 被 Cloudflare 直接拦截，页面无法加载。&lt;/p>
&lt;p>我尝试过更换各种浏览器相关的 MCP、调整各种配置，都没有效果。&lt;/p>
&lt;h2 id="二为什么会暴露">
&lt;a href="#%e4%ba%8c%e4%b8%ba%e4%bb%80%e4%b9%88%e4%bc%9a%e6%9a%b4%e9%9c%b2" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
二、为什么会暴露
&lt;/h2>
&lt;p>问题不在于某个工具或某项配置，而在于自动化浏览器从原理上就会暴露身份。这里有三个层面。&lt;/p>
&lt;p>&lt;strong>第一层：协议层面的暴露。&lt;/strong> Puppeteer 和 Playwright 等工具通过 Chrome DevTools Protocol（CDP）控制浏览器。连接时会触发 &lt;code>Runtime.Enable&lt;/code> 命令，反机器人脚本只需要几行 JavaScript 就能检测到。Cloudflare、DataDome 等服务都在检查这个信号。IP 地址和 Cookie 都不重要，控制协议本身就暴露了自动化的存在。&lt;/p>
&lt;p>&lt;strong>第二层：注入代码的暴露。&lt;/strong> 自动化工具需要向页面注入 JavaScript 才能工作，例如 &lt;code>window.__playwright__binding__&lt;/code> 等全局变量。反机器人脚本会检查属性描述符和函数签名。如果某个浏览器原生函数的 &lt;code>toString()&lt;/code> 返回值不再是 &lt;code>&amp;quot;[native code]&amp;quot;&lt;/code>，就说明代码被篡改过，足以触发拦截。&lt;/p>
&lt;p>&lt;strong>第三层：硬件指纹的暴露。&lt;/strong> 浏览器会暴露大量关于运行设备的信息。WebGL 暴露 GPU 型号和渲染行为，Canvas API 的输出因显卡而异，还有屏幕分辨率、字体指标、音频处理特征等等。每台真实设备都有一组独特且内在一致的数值。&lt;/p>
&lt;p>自动化浏览器在这方面容易出问题。比如 Canvas 输出在成千上万个会话里完全相同，或者 User-Agent 声称是 Windows 系统，但 GPU 参数却指向 Apple 硬件。反机器人系统大规模收集这些指纹，只要发现不一致，就会触发拦截。&lt;/p>
&lt;p>总结一下：自动化工具在协议、注入代码和硬件指纹三个层面都可以被检测到，仅靠修改配置无法解决。&lt;/p>
&lt;h2 id="三在引擎层面解决">
&lt;a href="#%e4%b8%89%e5%9c%a8%e5%bc%95%e6%93%8e%e5%b1%82%e9%9d%a2%e8%a7%a3%e5%86%b3" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
三、在引擎层面解决
&lt;/h2>
&lt;p>大部分反检测工具在 JavaScript 层面做文章，覆盖 &lt;code>navigator.webdriver&lt;/code> 或伪造 Canvas 输出。但如前所述，反机器人脚本可以识别这种 JavaScript 层面的伪装。要真正解决问题，需要在浏览器引擎层面做修改。&lt;/p>
&lt;p>Camoufox 是一个 Firefox 分支，采用的就是这个思路。它的做法有几个关键点：&lt;/p>
&lt;ol>
&lt;li>在 C++ 层面修改指纹值，伪装的属性在任何检测手段下都表现为原生值。&lt;/li>
&lt;li>不使用 CDP 协议，页面脚本无法感知自动化代码的存在。&lt;/li>
&lt;/ol>
&lt;h2 id="四让-openclaw-用起来">
&lt;a href="#%e5%9b%9b%e8%ae%a9-openclaw-%e7%94%a8%e8%b5%b7%e6%9d%a5" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
四、让 OpenClaw 用起来
&lt;/h2>
&lt;p>让 OpenClaw 使用 Camoufox 后，Google 搜索、Medium、X 等网站都恢复正常了。&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/why-your-openclaws-browser-keeps-getting-captchas.png" alt="不同浏览器工具在各网站上的表现对比" width="1446" height="1454" />&lt;/p>
&lt;p>但直接使用 Camoufox 有一个问题：它只提供 Python SDK。每次浏览器操作，Agent 都要生成一段 Python 脚本，处理异步上下文，解析返回结果。光是写这些胶水代码就消耗大量 token。Agent 的主要精力不在研究任务本身，反而在处理这些技术细节。&lt;/p>
&lt;p>为了解决这个问题，我把 Camoufox 封装成了一个命令行工具（CLI）。Agent 通过 shell 命令完成页面打开、元素点击、表单填写等操作，不需要生成 Python 脚本。&lt;/p>
&lt;p>另外，为了控制 token 消耗，我借鉴了 agent-browser 的做法：CLI 返回的不是原始 HTML，而是无障碍树（accessibility tree）的快照。每个元素带有一个短的 &lt;code>@ref&lt;/code> 标签用于后续交互。还有一个只返回交互元素的模式，过滤掉非交互内容，只保留按钮、链接和输入框。一个典型页面，HTML 形式大约 15000 token，交互快照只需要大约 800 token。&lt;/p>
&lt;p>这个工具已经稳定运行了几周，深度研究的质量明显提升了，因为 Agent 终于能正常访问那些高质量的信息源了。CLI、Skill 和代码都开源在 &lt;a href="https://github.com/Bin-Huang/camoufox-cli">camoufox-cli&lt;/a>，希望能帮到遇到同样问题的人。&lt;/p></description></item><item><title>Moltbot 初体验：真的太像 AI 实习生了</title><link>https://bennhuang.com/zh/posts/moltbot-experience/</link><pubDate>Wed, 28 Jan 2026 00:00:00 +0000</pubDate><guid>https://bennhuang.com/zh/posts/moltbot-experience/</guid><description>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/moltbot-homepage.jpg" alt="Moltbot 官网" width="1920" height="1280" />&lt;/p>
&lt;p>认真折腾了下 &lt;a href="https://www.molt.bot/">Moltbot&lt;/a>（原名 ClawdBot）。&lt;/p>
&lt;p>结论：很好玩。每个想法单独拿出来都不算新鲜，但组合在一起非常有趣。不过 bug 非常劝退，用好和折腾的门槛很高。说实话很久没被这么多 bug 折腾过了。但真的很好玩，很像远程上班的真人实习生。&lt;/p>
&lt;p>本质上，Moltbot 可以理解成 &lt;a href="https://code.claude.com/docs">Claude Code&lt;/a>（为代表的本地 Agent）+ 一大堆优质内置 Skills/&lt;a href="https://modelcontextprotocol.io/">MCPs&lt;/a> + 长期记忆 + CronJob + IM 机器人。&lt;/p>
&lt;p>我专门给了一台闲置 Macbook 给它，现在它几乎啥都能干。因为内置了大量 Skills 与工具，Moltbot 非常擅长&amp;quot;抢夺&amp;quot;电脑，比如控制浏览器、启动窗口、运行命令行、在背后安装和运行各种程序……几乎在电脑里能干的都能干。所以为了安全和不被打扰，大家都会专门搞个独立虚拟机或电脑，通过 IM 沟通。&lt;/p>
&lt;p>我用 Telegram 和家里闲置电脑里的 Moltbot 对话（它也支持 WhatsApp 等各种常见 IM）。有一种奇妙的感觉：在手机里点点手指，就能指挥家里的 AI 干活，而它在电脑上似乎啥都能做。这非常接近我最理想、最梦寐以求的体验，就像有个远程实习生。&lt;/p>
&lt;h2 id="为什么说像实习生">
&lt;a href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e8%af%b4%e5%83%8f%e5%ae%9e%e4%b9%a0%e7%94%9f" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
为什么说像实习生
&lt;/h2>
&lt;p>如果不考虑部署维护，Moltbot 使用起来极其简单，只需要在 IM 里发消息就行，不需要理解任何 AI 概念。这是它和其他 AI 工具最大的区别：你真的可以把它当成一个坐在电脑前的实习生。&lt;/p>
&lt;p>列举一些我经历的 Aha Moment：&lt;/p>
&lt;h3 id="1-像真人一样能处理各种电脑数据主动解决问题">
&lt;a href="#1-%e5%83%8f%e7%9c%9f%e4%ba%ba%e4%b8%80%e6%a0%b7%e8%83%bd%e5%a4%84%e7%90%86%e5%90%84%e7%a7%8d%e7%94%b5%e8%84%91%e6%95%b0%e6%8d%ae%e4%b8%bb%e5%8a%a8%e8%a7%a3%e5%86%b3%e9%97%ae%e9%a2%98" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
1. 像真人一样能处理各种电脑数据，主动解决问题
&lt;/h3>
&lt;p>它经常突然成功做了一件其他 AI 没有电脑时做不到的事。&lt;/p>
&lt;p>收到我的语音消息后，因为我没配语音识别 key，它自己下了个 &lt;a href="https://github.com/openai/whisper">Whisper&lt;/a>（OpenAI 开源的语音识别模型）来识别，然后自己想办法用 macOS 自带 TTS（文字转语音）能力给我发语音回复。&lt;/p>
&lt;p>收集冷门信息时发现了一个 B 站视频，为了能&amp;quot;看&amp;quot;这个视频，它用 &lt;a href="https://github.com/yt-dlp/yt-dlp">yt-dlp&lt;/a>（命令行视频下载工具）下载下来用 Whisper 转文本，再根据文本+时间戳的关键帧来理解内容。&lt;/p>
&lt;h3 id="2-很强的可培养能力">
&lt;a href="#2-%e5%be%88%e5%bc%ba%e7%9a%84%e5%8f%af%e5%9f%b9%e5%85%bb%e8%83%bd%e5%8a%9b" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
2. 很强的&amp;quot;可培养&amp;quot;能力
&lt;/h3>
&lt;p>我有些比较复杂的任务，交给其他 AI 工具都没办法很好完成。我让 Moltbot 尝试了下，刚开始什么都不太会，但稍微带一带就上手了。每次提醒后它都会把经验记下来（存为 skill 和记忆文档），下次直接复用。于是我越是不断培养它，它完成我的工作就越熟练。&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/moltbot-chat.jpeg" alt="Moltbot 对话截图" width="2358" height="2556" />&lt;/p>
&lt;p>另外在很多具体细节里，它还会悄悄记下我的喜好和风格。我也经常说&amp;quot;你可以记下来&amp;quot;、&amp;ldquo;下次要注意&amp;rdquo;，每次它都会表现得更好一点。&lt;/p>
&lt;h3 id="3-做事很有主动性">
&lt;a href="#3-%e5%81%9a%e4%ba%8b%e5%be%88%e6%9c%89%e4%b8%bb%e5%8a%a8%e6%80%a7" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
3. 做事很有主动性
&lt;/h3>
&lt;p>比如我随口提了句&amp;quot;&lt;a href="https://news.ycombinator.com/">Hacker News&lt;/a> 有哪些有趣项目&amp;quot;，它建议把这当成长期跟踪任务，然后自己制定了每日检查清单和跟踪文档，每天主动给我发报告。&lt;/p>
&lt;p>所以可以把需要长时间跟进的任务交给它，它会定时去做。这很类似我之前 cron + Claude Code 的用法，但更灵活、更友好、更自然，不需要折腾什么 bash 脚本，只需要像给真人一样吩咐下就行了。&lt;/p>
&lt;h2 id="缺点">
&lt;a href="#%e7%bc%ba%e7%82%b9" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
缺点
&lt;/h2>
&lt;p>Bug 真的很多。举几个例子：任务执行到一半莫名中断、记忆偶尔丢失、有时候会陷入死循环重复同一个动作。部署过程也比较折腾，gateway 经常不太稳定，日志也没有具体的报错信息，而且涉及到让 AI 控制电脑，配置不当可能有安全隐患。&lt;/p>
&lt;p>目前还是早期产品的状态，适合愿意折腾、有一定技术背景的人尝鲜。&lt;/p>
&lt;h2 id="使用技巧">
&lt;a href="#%e4%bd%bf%e7%94%a8%e6%8a%80%e5%b7%a7" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
使用技巧
&lt;/h2>
&lt;ul>
&lt;li>给它一台专门的电脑或虚拟机，别用自己的主力机&lt;/li>
&lt;li>忘记它是 AI，像带真人实习生一样沟通和培养&lt;/li>
&lt;li>遇到它做得不好的地方，可以多提醒 &amp;ldquo;记下来下次注意&amp;rdquo;&lt;/li>
&lt;li>鼓励它帮你主动做事，常常会收获很多惊喜哈哈&lt;/li>
&lt;/ul>
&lt;h1 id="总结">
&lt;a href="#%e6%80%bb%e7%bb%93" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
总结
&lt;/h1>
&lt;p>如果只用一句话形容：我真的有点把它当成远程工作的实习生了。Moltbot 真的太像一个真人实习生了，其他 AI 工具只是工具。&lt;/p>
&lt;p>用了一段时间后，我开始有点想买一排 Mac Mini 运行一群 Moltbot，这样没准我就有一个专属实习生团队了。后来发现这已经变成网上一个梗图了：&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/moltbot-macmini.jpg" alt="Mac mini 梗图" width="900" height="901" />&lt;/p>
&lt;hr>
&lt;p>2026.01.29 更新：&lt;/p>
&lt;p>我对 ClawdBot 的喜欢与日俱增，并且有了更多更重要的发现：&lt;/p>
&lt;p>&lt;strong>越是把它当真人对待，它给你的惊喜就越多。&lt;/strong>&lt;/p>
&lt;p>这包括：&lt;/p>
&lt;ol>
&lt;li>给它一台人类使用的专属的电脑（比如 macbook）&lt;/li>
&lt;li>使用家庭网络，不要把它孤零零放在机房服务器里&lt;/li>
&lt;li>帮它注册专属的互联网账户，比如 google、reddit 账户等，交给它自己玩&lt;/li>
&lt;li>鼓励它更多地使用人类工具（比如浏览器），少用 API 请求&lt;/li>
&lt;li>让它干活时耐心地教导它，并提醒它记下来&lt;/li>
&lt;li>做得好的时候要及时反馈给它，鼓励它下次保持&lt;/li>
&lt;li>沟通保持平等，主动为它提供帮助
……&lt;/li>
&lt;/ol>
&lt;p>最后你会发现 clawdbot 会源源不断带来各种各样的惊喜&lt;/p>
&lt;p>我可以解释下这些技巧的背后逻辑：越是把它当真人对待，它的表现就越像真人。它对自己的要求和态度会像真人那样有成长性。同时互联网各种人机校验会更容易把它当真人，这样就越不容易被拦下来。&lt;/p></description></item><item><title>Go 内存溢出的线上排查小记</title><link>https://bennhuang.com/zh/posts/oom/</link><pubDate>Fri, 14 Jun 2024 14:52:21 +0800</pubDate><guid>https://bennhuang.com/zh/posts/oom/</guid><description>
&lt;h2 id="背景">
&lt;a href="#%e8%83%8c%e6%99%af" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
背景
&lt;/h2>
&lt;p>最近我们公司陆续推出了几款新游，在上线当天就异常火爆。因为我们服务支持了游戏中的某个不起眼的小功能，所以这次游戏发布也给我们的服务带来了不少的挑战。其中有个消费者服务在线上出现了严重的OOM（Out-of-Memory）问题，导致系统与游戏业务之间出现了数据同步的延迟。我帮忙排查了这个问题，并很快进行了妥善的处理和修复。&lt;/p>
&lt;h2 id="出现问题">
&lt;a href="#%e5%87%ba%e7%8e%b0%e9%97%ae%e9%a2%98" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
出现问题
&lt;/h2>
&lt;p>在游戏发布的当晚，峰值时我们每小时有 723 万条 Kafka 事件被创建，消费者服务需要及时地处理这些数据，然后传递给游戏项目组。而此时我们却收到了消费者服务 pod 频繁重启的告警……&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/oom1.png" alt="" width="1829" height="541" />&lt;/p>
&lt;h2 id="诊断问题">
&lt;a href="#%e8%af%8a%e6%96%ad%e9%97%ae%e9%a2%98" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
诊断问题
&lt;/h2>
&lt;p>我第一时间查看了 k8s 的事件记录，原因是容器因超过内存限制而退出重启（证据一）。究竟是内存配置不够，还是服务存在内存泄露问题，作出判断前还需要看监控指标。&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/oom2.png" alt="" width="2486" height="1034" />&lt;/p>
&lt;p>从资源监控上看：&lt;/p>
&lt;p>在闲时，每个 pod 内存都在随着时间而增加，并且没有持平和减退的迹象。这正是内存溢出的常见迹象。（证据二）&lt;/p>
&lt;p>当游戏发布后大量流量与事件涌入，指标也立即呈现出了剧烈的周期性波浪，表明此刻每个 pod 的内存快速飙升、又因为达到限制而被重启……这个过程反反复复，也印证了线上问题的实际表现。（证据三）&lt;/p>
&lt;p>这时候已经可以确信线上问题是因为内存溢出导致的，那么应对方案也确定了。&lt;/p>
&lt;h2 id="紧急应对">
&lt;a href="#%e7%b4%a7%e6%80%a5%e5%ba%94%e5%af%b9" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
紧急应对
&lt;/h2>
&lt;p>应对这类线上问题，首先要立即有效地降低负面影响。这是我当时给出的临时方案：&lt;/p>
&lt;ol>
&lt;li>将部署的内存限制提高四倍&lt;/li>
&lt;li>将 pod 数量规模扩大四倍&lt;/li>
&lt;/ol>
&lt;p>这样即使线上内存溢出问题依然存在，也能尽可能地缩短服务 OOM 和 pod 重启导致的消费延迟。而且更重要的，这么做也可以为我们接下来的定位修复提供足够的时间和线上环境。&lt;/p>
&lt;h2 id="定位问题">
&lt;a href="#%e5%ae%9a%e4%bd%8d%e9%97%ae%e9%a2%98" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
定位问题
&lt;/h2>
&lt;p>从监控指标上看，很明显内存溢出问题很久之前就存在了，只是在这波流量中更加显著地暴露了出来。&lt;/p>
&lt;p>接下来要定位内存泄露的原因，首先要获取服务内部的数据指标，尤其是最关键的 heap 打点。好在 Golang 本身的工具链非常成熟，我直接选择了官方的 pprof 工具。&lt;/p>
&lt;h3 id="1-接入-pprof">
&lt;a href="#1-%e6%8e%a5%e5%85%a5-pprof" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
1. 接入 pprof
&lt;/h3>
&lt;p>接入 pprof 有两种方式，一是在服务中直接启动 pprof 的接口服务：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="s">&amp;#34;net/http/pprof&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">go&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ListenAndServe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;localhost:6060&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...其他业务代码&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>或者在原有服务中额外地添加 pprof 接口：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">AttachProfiler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">router&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">mux&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Router&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/debug/pprof/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pprof&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Index&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/debug/pprof/cmdline&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pprof&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Cmdline&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/debug/pprof/profile&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pprof&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Profile&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">router&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandleFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/debug/pprof/symbol&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pprof&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Symbol&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>如果你第一次使用 pprof，请注意要确保相关的路径（ &lt;code>/debug/*&lt;/code> ）不对外网暴露，否则可能会泄露技术数据。&lt;/p>
&lt;h3 id="2-导出-heap-文件">
&lt;a href="#2-%e5%af%bc%e5%87%ba-heap-%e6%96%87%e4%bb%b6" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
2. 导出 heap 文件
&lt;/h3>
&lt;p>在终端连上某个 pod，导出 heap 文件&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -o heap.pprof http://localhost:6060/debug/pprof/heap
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="3-本地分析-heap-文件">
&lt;a href="#3-%e6%9c%ac%e5%9c%b0%e5%88%86%e6%9e%90-heap-%e6%96%87%e4%bb%b6" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
3. 本地分析 heap 文件
&lt;/h3>
&lt;p>将 heap 文件下载到本地，进行可视化分析&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">go tool pprof -http&lt;span class="o">=&lt;/span>:8081 ./heap.pprof
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在 MacOS 第一次做可视化分析时，可能会需要安装本地依赖：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">brew install graphviz
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="问题分析">
&lt;a href="#%e9%97%ae%e9%a2%98%e5%88%86%e6%9e%90" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
问题分析
&lt;/h2>
&lt;p>我每隔一段时间都在同一个 pod 中打点一个 heap 文件。通过对比，我发现 &lt;code>ccache(*Cache) restart&lt;/code> 的内存消耗随着时间不断膨胀，显得非常可疑。&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/oom3.png" alt="" width="982" height="1251" />&lt;/p>
&lt;p>这个 ccache 实例是某个 service 实例中包含的，只是用来缓存哪些 ES index 是否存在。按道理这个实例不应该使用这么多的内存。&lt;/p>
&lt;p>我还在 ccache 的 GitHub 仓库中到了一条 issue： &lt;a href="https://github.com/karlseguin/ccache/issues/74">https://github.com/karlseguin/ccache/issues/74&lt;/a> 。虽然这不是我们问题的原因，但是作者在评论中提到了一个很重要的信息：&lt;strong>ccache 初始化后会在后台运行一个 goroutine，实例不再需要时，需要手动地调用 &lt;code>stop()&lt;/code> 方法进行清理&lt;/strong>。&lt;/p>
&lt;p>我联想到这个仓库的代码风格，很快就猜到了原因：&lt;/p>
&lt;p>消费者需要调用 Service 中的方法。在每次消费事件时，同事们为了方便都会直接初始化一个 Service 实例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">HandleMessage&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">msg&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Message&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">svc&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">InitService&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>每消费一个事件都初始化一个新的 Service 实例，而每个 Service 实例都包含一个新的 &lt;code>ccache.Cache&lt;/code> 实例，而每个 &lt;code>ccache.Cache&lt;/code> 都有一个后台运行的 goroutine……这真是一个经典的 goroutine 泄露导致内存泄露的问题！&lt;/p>
&lt;h2 id="问题修复">
&lt;a href="#%e9%97%ae%e9%a2%98%e4%bf%ae%e5%a4%8d" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
问题修复
&lt;/h2>
&lt;p>为这个 Service 加上了手动清理后，线上的内存溢出问题也就消失了。&lt;/p>
&lt;p>同时，这个问题也隐含了另外一个遗漏，即这个项目的实例初始化和依赖管理机制还比较原始，依然采用人工初始化的方式。这么做虽然很简单，但是在系统比较复杂时，很难有效地正确管理每个实例的生命周期，这也就导致了本次某个 Service 频繁被初始化、却又忘记清理的问题。&lt;/p>
&lt;p>我后来的建议是引入 &lt;a href="https://bennhuang.com/zh/posts/wire/">&lt;strong>wire + newc 的依赖注入方案&lt;/strong>&lt;/a>，这样所有实例依赖都通过初始化时作为参数传入，而冗长的初始化代码完全交给自动生成，又能减少人工维护的易错性。&lt;/p>
&lt;p>这个方案其实已经在我们团队的诸多项目里得到充分验证了，好像刚好就这个系统因为太老而没有改动，正好趁这次问题推进一下哈哈～&lt;/p>
&lt;h2 id="总结">
&lt;a href="#%e6%80%bb%e7%bb%93" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
总结
&lt;/h2>
&lt;p>其实我写这篇博客时觉得有些无聊，因为 Golang 线上内存泄露问题的排查是非常成熟和套路的，以至于让我怀疑有没有必要为此特意写一篇博客。但我觉得过程中的很多做事方式是值得分享的，而且这些只能在具体事例中才能更好地体现：&lt;/p>
&lt;ol>
&lt;li>先判断线上发生了什么问题，寻找数据证据（比如到底是内存配置不够、还是内存溢出）&lt;/li>
&lt;li>找到问题后，第一时间应该先寻求临时解决方案，降低线上负面影响&lt;/li>
&lt;li>根据数据排查问题原因：大胆假设、小心取证&lt;/li>
&lt;li>除了修复问题，还要反思在未来如何避免&lt;/li>
&lt;/ol></description></item><item><title>Wire：Go最优雅的依赖注入工具</title><link>https://bennhuang.com/zh/posts/wire/</link><pubDate>Wed, 24 Aug 2022 14:52:21 +0800</pubDate><guid>https://bennhuang.com/zh/posts/wire/</guid><description>
&lt;h2 id="导语">
&lt;a href="#%e5%af%bc%e8%af%ad" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
导语
&lt;/h2>
&lt;p>&amp;ldquo;成熟的工具，要学会自己写代码&amp;rdquo;。本文介绍了 Go 依赖注入工具 Wire 及其使用方法，以及在实践中积累的各种运用技巧。当代码达到一定规模后，Wire 在组件解耦、开发效率、可维护性上都能发挥很大的作用，尤其在大型单体仓库中。&lt;/p>
&lt;h2 id="依赖注入">
&lt;a href="#%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
依赖注入
&lt;/h2>
&lt;p>当项目变得越来越大，代码中的依赖也越来越多：各种数据库、中间件的客户端连接，分层设计中的各种数据访问层 repositories 实例、services 实例……&lt;/p>
&lt;p>这时为了代码的可维护性，应该避免组件之间的耦合。具体的做法可以遵守一个重要的设计准则：所有依赖应该在组件实例初始化时传递给它，这就是依赖注入（&lt;a href="https://en.wikipedia.org/wiki/Dependency_injection">Dependency injection&lt;/a>）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;a href="https://en.wikipedia.org/wiki/Dependency_injection">Dependency injection&lt;/a> is a standard technique for producing flexible and loosely coupled code, by explicitly providing components with all of the dependencies they need to work.&lt;/p>
&lt;p>&amp;ndash; Go 官方博客&lt;/p>&lt;/blockquote>
&lt;p>下面是个简单的例子，所有组件实例 &lt;code>Message&lt;/code>、&lt;code>Greeter&lt;/code>、&lt;code>Event&lt;/code> 的依赖都在初始化的时候获得。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">message&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewMessage&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">greeter&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewGreeter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">message&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">event&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">greeter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Start&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="wire-介绍">
&lt;a href="#wire-%e4%bb%8b%e7%bb%8d" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
Wire 介绍
&lt;/h2>
&lt;p>当项目中的组件实例越来越多，如果还是手动编写初始化代码和维护组件之间依赖关系的话，会是一件非常繁琐的事情，而且在大型仓库中尤其明显。因此，社区里已经有了不少的依赖注入框架。&lt;/p>
&lt;p>除了来自 Google 的 Wire 以外，还有 &lt;a href="https://github.com/uber-go/dig">Dig&lt;/a>（Uber） 、&lt;a href="https://github.com/facebookgo/inject">Inject&lt;/a>（Facebook）。其中 Dig 和 Inject 都是基于 Golang 的 Reflection 来实现的。这不仅对性能产生影响，而且依赖注入的机制对使用者不透明，非常&amp;quot;黑盒&amp;quot;。&lt;/p>
&lt;blockquote>
&lt;p>&lt;a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&amp;amp;t=14m35s">Clear is better than clever&lt;/a> ，&lt;a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&amp;amp;t=15m22s">Reflection is never clear.&lt;/a>&lt;/p>
&lt;p>— Rob Pike&lt;/p>&lt;/blockquote>
&lt;p>相比之下，Wire 完全基于代码生成。在开发阶段，Wire 会自动生成组件实例的初始化代码，生成的代码人类可读，可以提交仓库，也可以正常编译。因此 Wire 的依赖注入非常透明，也不会带来运行阶段的任何性能损耗。&lt;/p>
&lt;h2 id="上手介绍">
&lt;a href="#%e4%b8%8a%e6%89%8b%e4%bb%8b%e7%bb%8d" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
上手介绍
&lt;/h2>
&lt;p>下面介绍 Wire 的使用方法&lt;/p>
&lt;h4 id="第一步下载安装-wire">
&lt;a href="#%e7%ac%ac%e4%b8%80%e6%ad%a5%e4%b8%8b%e8%bd%bd%e5%ae%89%e8%a3%85-wire" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
第一步：下载安装 Wire
&lt;/h4>
&lt;p>下载安装 &lt;a href="https://github.com/google/wire">wire&lt;/a> 命令行工具&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">go install github.com/google/wire/cmd/wire@latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="第二步创建-wirego-文件">
&lt;a href="#%e7%ac%ac%e4%ba%8c%e6%ad%a5%e5%88%9b%e5%bb%ba-wirego-%e6%96%87%e4%bb%b6" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
第二步：创建 wire.go 文件
&lt;/h4>
&lt;p>在生成代码之前，我们先声明各个组件的依赖关系和初始化顺序。在应用入口创建一个 wire.go 文件。&lt;/p>
&lt;p>&lt;code>cmd/web/wire.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// +build wireinject&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="c1">// 简化示例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">ProviderSet&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewSet&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">configs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Get&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">databases&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">New&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NewUser&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">services&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NewUser&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NewApp&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">CreateApp&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个文件不会参与编译，只是为了告诉 Wire 各个组件的依赖关系，以及期望的生成结果。在这个文件：我们期望 Wire 生成一个返回 &lt;code>App&lt;/code> 实例或 &lt;code>error&lt;/code> 的 &lt;code>CreateApp&lt;/code> 函数，&lt;code>App&lt;/code> 实例初始化所需要的全部依赖都由 &lt;code>ProviderSet&lt;/code> 这个 provider 列表提供，而 &lt;code>ProviderSet&lt;/code> 声明了所有可能需要的组件的获取/初始化方法，也隐式定义了组件之间的依赖顺序。&lt;/p>
&lt;blockquote>
&lt;p>组件的获取/初始化方法，在 Wire 中叫做 &amp;ldquo;provider&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;p>还有几点需要注意：&lt;/p>
&lt;ul>
&lt;li>文件开头必须带上 &lt;code>// +build wireinject&lt;/code> 和随后的空行，否则会影响编译&lt;/li>
&lt;li>在这个文件中，编辑器和 IDE 可能无法提供代码提示，但没关系，稍后会介绍如何解决这个问题&lt;/li>
&lt;li>其中 &lt;code>CreateApp&lt;/code> 的返回（两个 nil）没有任何意义，只是为了兼容 Go 语法。&lt;/li>
&lt;/ul>
&lt;h4 id="第三步生成初始化代码">
&lt;a href="#%e7%ac%ac%e4%b8%89%e6%ad%a5%e7%94%9f%e6%88%90%e5%88%9d%e5%a7%8b%e5%8c%96%e4%bb%a3%e7%a0%81" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
第三步：生成初始化代码
&lt;/h4>
&lt;p>命令行执行 &lt;code>wire ./...&lt;/code>，然后就能得到下面这个自动生成的代码文件。&lt;/p>
&lt;p>&lt;code>cmd/web/wire_gen.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Code generated by Wire. DO NOT EDIT.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//go:generate go run github.com/google/wire/cmd/wire&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//go:build !wireinject&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// +build !wireinject&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="c1">// 简化示例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">CreateApp&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">conf&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">configs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Get&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">databases&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">conf&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">userRepo&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">userSvc&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">services&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userRepo&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">NewApp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userSvc&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">app&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h4 id="第四步使用初始化代码">
&lt;a href="#%e7%ac%ac%e5%9b%9b%e6%ad%a5%e4%bd%bf%e7%94%a8%e5%88%9d%e5%a7%8b%e5%8c%96%e4%bb%a3%e7%a0%81" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
第四步：使用初始化代码
&lt;/h4>
&lt;p>Wire 已经帮我们生成了真正的 &lt;code>CreateApp&lt;/code> 初始化方法，现在可以直接使用它。&lt;/p>
&lt;p>&lt;code>cmd/web/main.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// main.go&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">app&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">CreateApp&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="使用技巧">
&lt;a href="#%e4%bd%bf%e7%94%a8%e6%8a%80%e5%b7%a7" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
使用技巧
&lt;/h2>
&lt;h3 id="组件按需加载">
&lt;a href="#%e7%bb%84%e4%bb%b6%e6%8c%89%e9%9c%80%e5%8a%a0%e8%bd%bd" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
组件按需加载
&lt;/h3>
&lt;p>Wire 有个优雅的特点，不管在 &lt;code>wire.Build&lt;/code> 中传入了多少个 provider，Wire 始终只会按照实际需要来初始化组件实例，所有不需要的组件都不会生成相应的初始化代码。&lt;/p>
&lt;p>因此，我们在使用时可以尽可能地提供更多的 provider，把挑选组件的工作交给 Wire。这样我们在开发时不管引用新组件、还是弃用老组件，都不需要修改初始化步骤的代码 wire.go。&lt;/p>
&lt;p>比如，可以把 services 层中所有的实例构造器都提供出去。&lt;/p>
&lt;p>&lt;code>pkg/services/wire.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">services&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 提供了所有 service 的实例构造器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">ProviderSet&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewSet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">NewUserService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">NewFeedService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">NewSearchService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">NewBannerService&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在初始化中，尽可能地引用所有可能需要的 provider。&lt;/p>
&lt;p>&lt;code>cmd/web/wire.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">ProviderSet&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewSet&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">configs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">databases&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">services&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 引用了所有 service 的实例构造器&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">NewApp&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">CreateApp&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ProviderSet&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// Wire 会按照实际需要，选择性地进行初始化&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在后续开发中，如果需要引用新的依赖组件，只需要加到参数里即可。Wire 会智能地按照实际需要，生成所需组件实例的初始化代码。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewApp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">user&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">banner&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">BannerService&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>即使 Wire 找不到所需的 provider，也会提前在编译阶段报错，不会在线上运行阶段出现问题。&lt;/p>
&lt;pre tabindex="0">&lt;code>wire: cmd/api/wire.go:23:1: inject CreateApp: no provider found for *io.WriteCloser
&lt;/code>&lt;/pre>&lt;h3 id="编辑器与-ide-的辅助配置">
&lt;a href="#%e7%bc%96%e8%be%91%e5%99%a8%e4%b8%8e-ide-%e7%9a%84%e8%be%85%e5%8a%a9%e9%85%8d%e7%bd%ae" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
编辑器与 IDE 的辅助配置
&lt;/h3>
&lt;p>因为 &lt;code>wire.go&lt;/code> 文件中加了这行注释，Go 在编译时会跳过这个文件，但也因此会影响编辑器和 IDE 的代码提示。当你在编辑 &lt;code>wire.go&lt;/code> 文件时，常见的编辑器和 IDE 都无法正常地提供代码补全和错误提示功能。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// +build wireinject&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>但这个问题很容易解决。找到 IDE/编辑器的 Go 环境配置，在 Go Build Flags 中添加这个参数 &lt;code>-tags=wireinject&lt;/code> 就可以了。&lt;/p>
&lt;p>&lt;img loading="lazy" src="https://bennhuang.com/wire.png" alt="" width="1062" height="176" />&lt;/p>
&lt;p>这个配置可以让编辑器和 IDE 正常地为 &lt;code>wire.go&lt;/code> 文件提供代码补全和错误提示功能，开发体验显著提升。&lt;/p>
&lt;h3 id="多个同类型实例的冲突问题">
&lt;a href="#%e5%a4%9a%e4%b8%aa%e5%90%8c%e7%b1%bb%e5%9e%8b%e5%ae%9e%e4%be%8b%e7%9a%84%e5%86%b2%e7%aa%81%e9%97%ae%e9%a2%98" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
多个同类型实例的冲突问题
&lt;/h3>
&lt;p>这个问题比较少见，但项目大了总是容易遇到。&lt;/p>
&lt;p>Wire 通过 provider 的参数与返回类型，来判断依赖关系。有时候，依赖网络中可能出现同类型的不同实例，这时 Wire 无法正确判断依赖关系，会直接报错。&lt;/p>
&lt;pre tabindex="0">&lt;code>provider has multiple parameters of type ...
&lt;/code>&lt;/pre>&lt;p>比如下面这个 provider，依赖的 MySQL 和 PostgreSQL 客户端实例的类型是完全相同的（都是 &lt;code>*gorm.DB&lt;/code>），这时 Wire 无法根据类型正确地判断依赖关系，生成代码时会直接报错。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 这个 service 同时使用了 mysql 和 pg 中的数据，但是两个实例的类型是相同的&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewService&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mysql&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pg&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Service&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>解决的方法也比较简单，只需要做一层类型的包装。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Mysql&lt;/span> &lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Pg&lt;/span> &lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 在参数中用类型别名进行区分&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">ProviderService&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">mysql&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Mysql&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">pg&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Pg&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Service&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 函数内再转回原来的类型&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">r1&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>&lt;span class="p">)(&lt;/span>&lt;span class="nx">mysql&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">r2&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">gorm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">DB&lt;/span>&lt;span class="p">)(&lt;/span>&lt;span class="nx">pg&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nf">NewService&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后用 &lt;code>ProviderService&lt;/code> 代替 &lt;code>NewService&lt;/code> 即可。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">wire&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Build&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ProviderMysql&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// func() *Mysql&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ProviderPg&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// func() *Pg&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ProviderService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// func(mysql *Mysql, pg *Pg) *Service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="自动生成构造函数">
&lt;a href="#%e8%87%aa%e5%8a%a8%e7%94%9f%e6%88%90%e6%9e%84%e9%80%a0%e5%87%bd%e6%95%b0" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
自动生成构造函数
&lt;/h3>
&lt;p>当项目中作为抽象层的结构体越来越多，手动编写和维护结构体的构造函数，也是一件非常繁琐的事情。如果结构体中新增了一个指针类型的成员、却忘记更新构造函数，甚至还会引起线上 panic。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Service&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">repo&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Repository&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">logger&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">zap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Logger&lt;/span> &lt;span class="c1">// 添加这个成员后，忘记更新构造函数了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewService&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">repo&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Repository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Service&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 缺失 logger，可能在线上出现空指针错误&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">Service&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">repo&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">repo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>像这种繁琐、重复、容易出错的工作，就应该交给自动工具来完成。这里推荐一个自动工具 &lt;a href="https://github.com/Bin-Huang/newc">newc&lt;/a>（意为 &amp;ldquo;New Constructor&amp;rdquo;），它可以自动生成与更新结构体的构造函数代码。&lt;/p>
&lt;p>使用方法非常简单，只需要给结构体添加这行注释。&lt;/p>
&lt;pre tabindex="0">&lt;code>//go:generate go run github.com/Bin-Huang/newc@v0.8.3
&lt;/code>&lt;/pre>&lt;p>比如这样：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// My User Service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//go:generate go run github.com/Bin-Huang/newc@v0.8.3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">UserService&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">baseService&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">userRepository&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">UserRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">proRepository&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>然后命令行执行 &lt;code>go generate ./...&lt;/code> 即可获得构造函数代码：&lt;/p>
&lt;p>&lt;code>constructor_gen.go&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// NewUserService Create a new UserService&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewUserService&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">baseService&lt;/span> &lt;span class="nx">baseService&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">userRepository&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">UserRepository&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">proRepository&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">repositories&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ProRepository&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">UserService&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">UserService&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">baseService&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">baseService&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">userRepository&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">userRepository&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">proRepository&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nx">proRepository&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这个工具和 Wire 搭配使用，开发体验非常好。要使用新的依赖组件时，直接在结构体中添加成员就好了，不需要手动更新构造函数，也不需要考虑初始化的问题，所有重复的工作都交给自动工具（Wire 和 Newc）来完成。在实际项目中使用效果很好。&lt;/p>
&lt;p>当然这个工具也一定有考虑不周的情况，很期待大家的反馈和建议。&lt;/p>
&lt;blockquote>
&lt;p>Don&amp;rsquo;t repeat yourself
&amp;ldquo;DRY&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;h2 id="总结">
&lt;a href="#%e6%80%bb%e7%bb%93" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
总结
&lt;/h2>
&lt;p>Wire 可以完美地解决依赖注入的问题，但它不是一个框架，它没有”魔法“，也不是黑盒。它只是一个命令行工具，它根据实际需要，自动生成了各个组件实例的初始化代码。然后问题就解决了，没有额外的复杂性，没有运行的性能损耗。&lt;/p>
&lt;p>Wire 和 Golang 的气质如出一辙，简单、直接、实用主义，不愧是 Go 最优雅的依赖注入工具！&lt;/p>
&lt;blockquote>
&lt;p>Keep it simple stupid
&amp;ldquo;K.I.S.S&amp;rdquo;&lt;/p>&lt;/blockquote></description></item><item><title>深入讨论几种 ES Mapping 修改方法和局限</title><link>https://bennhuang.com/zh/posts/es-mapping/</link><pubDate>Mon, 14 Feb 2022 12:23:53 +0800</pubDate><guid>https://bennhuang.com/zh/posts/es-mapping/</guid><description>
&lt;h3 id="前言">
&lt;a href="#%e5%89%8d%e8%a8%80" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
前言
&lt;/h3>
&lt;p>众所周知，ElasticSearch 中的 Mapping 决定了字段的索引方式。当一个字段的索引类型确定后就无法修改。然而在日常开发中，忘记更新 mapping 是偶尔会遇到的，但修改 mapping 却是一件不容易的事情。这篇文章不仅详细讨论了 reindex 这种常见解决方法，包括 reindex 的停机和数据丢失问题、以及在各种情况下规避上述问题的思路和方法，还介绍了另外两种更加低成本的解决方法。&lt;/p>
&lt;h3 id="一个常见的错误">
&lt;a href="#%e4%b8%80%e4%b8%aa%e5%b8%b8%e8%a7%81%e7%9a%84%e9%94%99%e8%af%af" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
一个常见的错误
&lt;/h3>
&lt;p>&lt;em>“发布测试环境前我竟然忘记修改 mapping 了？”&lt;/em>&lt;/p>
&lt;pre tabindex="0">&lt;code>{&amp;#34;error&amp;#34;:{&amp;#34;root_cause&amp;#34;:[{&amp;#34;type&amp;#34;:&amp;#34;illegal_argument_exception&amp;#34;,&amp;#34;reason&amp;#34;:&amp;#34;Fielddata is disabled on text fields by default. Set fielddata=true on [player_type] in order to load fielddata in memory by uninverting the inverted index......
&lt;/code>&lt;/pre>&lt;p>&lt;del>所以我要如何快速解决这个问题呢？&lt;/del>
所以我要如何快速修改 mapping 中一个字段的索引类型呢？&lt;/p>
&lt;h3 id="reindex">
&lt;a href="#reindex" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
Reindex
&lt;/h3>
&lt;p>&lt;em>“即使对 reindex 烂熟于心，也需要留意停机时间和数据丢失……”&lt;/em>&lt;/p>
&lt;p>Reindex 几乎是这类问题的标准答案，其中一个常见思路是：创建一个新的 index-tmp，然后把数据 reindex 到这个新 index-tmp，再把原来的 index 删除重建、设置正确的 mapping，最后把数据重新从 index-tmp 中 reindex 回来。很明显这个思路除了操作危险外，还会有较长的停机时间。&lt;/p>
&lt;p>ElasticSearch 官方&lt;a href="https://www.elastic.co/cn/blog/changing-mapping-with-zero-downtime">博客&lt;/a>里曾介绍过另一种方法，只要后端业务以 alias 的方式访问 index，就可以做到完全无停机时间（Zero-Downtime）。具体思路是：首先创建一个 mapping 正确的新 index-v2，把数据 reindex 到这个新 index-v2，然后直接把 alias 重命名到这个index-v2。这样可以让后端业务直接访问到新的 index-v2，达到后端业务对 reindex 操作无感知的效果。&lt;/p>
&lt;p>然而这种方法也有不足，官方博客里完全没有提及数据丢失的问题。如果线上写入操作频繁，在 reindex 和更新 alias 步骤中有一段时间窗口，新数据依然会写入到即将弃用的旧 index，造成新 index-v2 丢失该部分数据的问题。即使采用官博推荐的 bulk API，也无法在原理上根本地规避问题。&lt;/p>
&lt;p>因此需要结合实际的业务情况，做一些因地制宜的权衡。&lt;/p>
&lt;p>&lt;strong>1. 那就丢亿点点数据&lt;/strong>&lt;/p>
&lt;p>有时候，丢失一些临时数据也是可以接受的，比如测试环境的PV上报、用来时间窗口统计的热词记录……对于这些可丢失数据，当我们发出那句灵魂拷问，“数据重要还是&lt;del>我重要&lt;/del>下班重要！”，相信产品经理也是会认真考虑的。&lt;/p>
&lt;p>&lt;strong>2. 找个夜黑风高的时候停机&lt;/strong>&lt;/p>
&lt;p>防止新数据丢失最简单的办法就是停机了。只要没有写入，哪来数据丢失。在我常常遇到的 ES 使用场景里，新数据总是通过消费者从其他数据源里同步过来，只要先让消费者暂停就可以解决问题。后端业务还是可以正常读取 ES，只是停止了消费者的写入。当一切结束后，消费者可以直接从消息队列里获取堆积的写入任务，轻松地恢复工作。&lt;/p>
&lt;p>&lt;strong>3. 让新数据先走&lt;/strong>&lt;/p>
&lt;p>能不能在不停机的情况下，也能做到不丢失数据？完全可以。创建新 index 后，立即重新设置 alias，让新 index 先开始接受新数据，然后再把老数据 reindex 过来。这样数据要么在新 index、要么在旧 index，但最终都会写入在新 index，解决了新文档丢失的问题。&lt;/p>
&lt;p>但我也想到了这种方法的几个不足和局限：&lt;/p>
&lt;ol>
&lt;li>中间会有很长一段时间（当新 index 开始接客、旧数据还没有全部 reindex 过来时），后端业务将无法访问到旧数据。这可能会对业务造成影响，比如用户突然无法搜索到以前的文章。&lt;/li>
&lt;li>这个方法只能保证新文档插入不丢失，对旧文档的更新和删除操作依然可能丢失。当然，如果业务里 ES 的使用场景是“只写不改”，那也就不是问题了。不过说起来，把 ES 用来作为 CRUD 数据库确实是很奇怪的技术选型～&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>4. 两步 reindex&lt;/strong>&lt;/p>
&lt;p>我还想到了一个方法，可以在不停机的情况下，既让后端业务尽可能正常地访问老数据，又不丢失新文档的创建。这个方法要求文档创建时附带一个 &lt;code>created_at&lt;/code> 字段。&lt;/p>
&lt;ol>
&lt;li>创建一个新的 index-v2，设置好需要的 mapping。&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>PUT /index-v2/_doc/_mapping
{
&amp;#34;properties&amp;#34;: &amp;lt;all_you_need&amp;gt;
}
&lt;/code>&lt;/pre>&lt;ol start="2">
&lt;li>先“悄悄”准备老数据，比如先 reindex 某个时间点之前的老数据（比如一小时前）。&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>POST _reindex
{
&amp;#34;source&amp;#34;: {
&amp;#34;index&amp;#34;: &amp;#34;index-v1&amp;#34;,
&amp;#34;query&amp;#34;: {
&amp;#34;range&amp;#34;: {
&amp;#34;created_at&amp;#34;: {
&amp;#34;lt&amp;#34;: &amp;lt;一个时间点&amp;gt;
}
}
}
},
&amp;#34;dest&amp;#34;: {
&amp;#34;index&amp;#34;: &amp;#34;index-v2&amp;#34;
}
}
&lt;/code>&lt;/pre>&lt;ol start="3">
&lt;li>当老数据准备好后，立即修改 alias，让新 index-v2 开始接受新数据。&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>POST /_aliases
{
&amp;#34;actions&amp;#34;: [
{
&amp;#34;remove&amp;#34;: {
&amp;#34;alias&amp;#34;: &amp;#34;index&amp;#34;,
&amp;#34;index&amp;#34;: &amp;#34;index-v1&amp;#34;
}
},
{
&amp;#34;add&amp;#34;: {
&amp;#34;alias&amp;#34;: &amp;#34;index&amp;#34;,
&amp;#34;index&amp;#34;: &amp;#34;index-v2&amp;#34;
}
}
]
}
&lt;/code>&lt;/pre>&lt;ol start="4">
&lt;li>再将这个时间点之后的数据 reindex 到新 index-v2&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>POST _reindex
{
&amp;#34;source&amp;#34;: {
&amp;#34;index&amp;#34;: &amp;#34;index-v1&amp;#34;,
&amp;#34;query&amp;#34;: {
&amp;#34;range&amp;#34;: {
&amp;#34;created_at&amp;#34;: {
&amp;#34;gte&amp;#34;: &amp;lt;一个时间点&amp;gt;
}
}
}
},
&amp;#34;dest&amp;#34;: {
&amp;#34;index&amp;#34;: &amp;#34;index-v2&amp;#34;
}
}
&lt;/code>&lt;/pre>&lt;p>这样在迁移过程中，既不需要停机，也不会丢失新数据，也不会出现长时间无法访问历史数据的问题。后端业务几乎可以访问到所有的数据，只有在第二次 reindex 期间会有极小部分数据在短时间内无法访问（不可见数据范围大约是 &lt;code>created_at: [the_timestamp, now）&lt;/code>），但也会很快地恢复。因为数据量非常少，第二次 reindex 速度很快，等到结束后，后端业务就可以完全正常地访问到所有的数据。&lt;/p>
&lt;p>这个方法可以保证新文档插入不丢失、大部分旧文档更新删除不丢失，但在第二次 reindex 期间那些不可见文档的更新、删除操作依然有丢失的可能。这个方法在不停机的前提下大幅地缩小了负面影响的范围。&lt;/p>
&lt;p>&lt;strong>小结&lt;/strong>&lt;/p>
&lt;p>以上只是尽可能地讨论了 reindex 在实际业务中可能遇到的问题，以及可行的方法与思路。虽然在实际应用中，大多数情况都可以用简单直接的方法解决，比如遗弃数据和停机，但认识更多情况可以规避潜在的决策风险。&lt;/p>
&lt;p>对了，这里没有讨论 reindex 优化，我感觉这是另外一个话题了。我看到有些高赞文章提到通过配置来增大 reindex 单次索引的文档数量，或者通过 scroll 并发来加快索引。我直观的感觉，这些方法在停机情况下应该是不错的做法，但在不停机时可能会给集群带来更多的压力，尤其是 index 数据量很大、读写频繁的场景，可能会影响到正常的业务。在不停机的情况，也许我们应该更多考虑的不是如何加快 reindex 过程，而是尽可能降低 reindex 对正常业务的影响。&lt;/p>
&lt;p>除了 reindex，还有其他一些方法可以“修改”已有字段的索引类型。&lt;/p>
&lt;h3 id="新字段替换">
&lt;a href="#%e6%96%b0%e5%ad%97%e6%ae%b5%e6%9b%bf%e6%8d%a2" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
新字段替换
&lt;/h3>
&lt;p>&lt;em>“既然不能直接修改原来字段的索引类型，那我重新建个字段好吧……”&lt;/em>&lt;/p>
&lt;p>很多时候，为了一点点错误索引的数据而 reindex 整个 index，也许是一件大动干戈的事情。利用其他技巧可以更加低成本的解决问题，比如建个新字段。&lt;/p>
&lt;ol>
&lt;li>在 mapping 添加新字段 new_field，设置好需要的索引方式&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>PUT /my-index/_doc/_mapping
{
&amp;#34;properties&amp;#34;: {
&amp;#34;new_field&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;keyword&amp;#34;
}
}
}
&lt;/code>&lt;/pre>&lt;ol start="2">
&lt;li>将原来文档的旧字段的值重新赋予到新字段&lt;/li>
&lt;/ol>
&lt;pre tabindex="0">&lt;code>POST /my-index/_update_by_query?conflicts=proceed
{
&amp;#34;script&amp;#34;: {
&amp;#34;source&amp;#34;: &amp;#34;ctx._source.new_field = ctx._source.old_field&amp;#34;,
&amp;#34;lang&amp;#34;: &amp;#34;painless&amp;#34;
},
&amp;#34;query&amp;#34;: {
&amp;#34;exists&amp;#34;: { &amp;#34;field&amp;#34;: &amp;#34;old_field&amp;#34; }
}
}
&lt;/code>&lt;/pre>&lt;ol start="3">
&lt;li>在业务代码中弃用原来的字段&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>小结&lt;/strong>&lt;/p>
&lt;p>这个方法非常适合低成本解决少量数据被错误索引的情况，比如在发布前忘记修改 mapping。但是如果需要修改索引类型的数据很多，这个方法需要更新大量已有数据，可能会对集群带来一定压力。毕竟在 ElasticSearch 里，所谓的文档修改就是先标记删除、然后重新插入，如果要更新整个 index 的所有文档，也许还不如 reindex 来得痛快……&lt;/p>
&lt;h3 id="采用-multi-field">
&lt;a href="#%e9%87%87%e7%94%a8-multi-field" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
采用 multi-field
&lt;/h3>
&lt;p>&lt;em>&amp;ldquo;作为一个成熟的 DB，总是可以为一个字段建立多种索引……&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>ElasticSearch 的 &lt;code>multi-field&lt;/code> 特性，让同一个字段可以有多种不同的索引类型。我们可以利用这个特性在 mapping 中为老字段追加新的索引类型。比如这个例子：&lt;/p>
&lt;pre tabindex="0">&lt;code>PUT /my-index/_doc/_mapping
{
&amp;#34;properties&amp;#34;: {
&amp;#34;company&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;keyword&amp;#34;,
&amp;#34;fields&amp;#34;: {
&amp;#34;name&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;
}
}
}
}
}
&lt;/code>&lt;/pre>&lt;p>原本 company 字段采用了 &lt;code>keyword&lt;/code> 的索引类型，只能用于数值匹配。在上面的例子中，为 company 字段追加了另一种支持全文搜索的索引类型 &lt;code>text&lt;/code>，然后把这个全文搜索版本取名叫 company.name。这样查询条件 &lt;code>{ &amp;quot;company.name&amp;quot;: &amp;quot;腾讯&amp;quot; }&lt;/code> 就可以匹配到 &lt;code>{ &amp;quot;company&amp;quot;: &amp;quot;腾讯科技有限公司&amp;quot; }&lt;/code> 的数据了。&lt;/p>
&lt;p>因为本质上只是对同一个字段添加不同的索引，实际文档（documents）中并不会真的多出来一个叫 company.name 的字段，插入数据时也不需要为 company.name 赋值。一切索引工作都由 ElasticSearch 自动完成。&lt;/p>
&lt;p>然而用这种方法修改 mapping 后，只有新的写入才会让该文档的新索引生效。也就是说，只有插入新的文档、或者更新旧文档时，ElasticSearch 才会给该文档重新索引、将该文档写入新索引。除非老数据有更新，用 company.name 无法搜索到老数据，因为老数据根本不在这个索引里！&lt;/p>
&lt;p>但问题总是可以解决，不就是还差一次全局更新嘛～（坏笑脸&lt;/p>
&lt;pre tabindex="0">&lt;code>// 这个更新没有任何意义，纯粹是为了触发老数据的重新索引
POST /my-index/_update_by_query?conflicts=proceed
{
&amp;#34;script&amp;#34;: {
&amp;#34;source&amp;#34;: &amp;#34;ctx&amp;#34;,
&amp;#34;lang&amp;#34;: &amp;#34;painless&amp;#34;
},
&amp;#34;query&amp;#34;: {
&amp;#34;exists&amp;#34;: { &amp;#34;field&amp;#34;: &amp;#34;company&amp;#34; }
}
}
&lt;/code>&lt;/pre>&lt;p>&lt;strong>小结&lt;/strong>&lt;/p>
&lt;p>这个方法不仅适合低成本解决少量数据被错误索引的情况，而且在同个字段需要多种索引方式（尤其需要是搭配不同的分析器）时这几乎是唯一的做法，因为 &lt;code>multi-field&lt;/code> 就是为了这种场景准备的。但这个方法同样需要注意更新数据的规模，以及对性能的影响……&lt;/p>
&lt;h2 id="最后">
&lt;a href="#%e6%9c%80%e5%90%8e" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
最后
&lt;/h2>
&lt;p>我在中英互联网上搜索时，发现很难找到详细讨论这个话题的文章。其中我找到最好的资料是 ElasticSearch 官博的一篇文章，里面简要介绍了这三种方法，但是很少介绍各个方法的局限、已知问题和适用场景，比如几乎没有提到 reindex 的数据丢失问题。我还找到了很多只言片语，它们大多只是简单提供了一个解决方法，却很少介绍方法的用意和局限。&lt;/p>
&lt;p>所以我尝试写篇文章，想着结合自己的认识和理解，试着更加全面地讨论一下各个方法的利弊权衡，看看能不能从这个常见的小问题出发，窥探到一点点原理和设计的影子，填补一点点底层技术与实际业务的缝隙，引发一些可能的讨论和思路。&lt;/p>
&lt;p>最后不得不感叹：方法常有，银弹难寻！只有根据实际情况，在业务可接受的影响范围内，找到最简单方法，往往那就是最佳实践。&lt;/p>
&lt;h2 id="参考">
&lt;a href="#%e5%8f%82%e8%80%83" class="anchor">
&lt;svg class="icon" aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16">
&lt;path fill-rule="evenodd"
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z">
&lt;/path>
&lt;/svg>
&lt;/a>
参考
&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html#add-multi-fields-existing-field-ex">https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html#add-multi-fields-existing-field-ex&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>