<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.0.1">Jekyll</generator><link href="https://imageslr.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://imageslr.com/" rel="alternate" type="text/html" /><updated>2025-09-01T14:30:24+08:00</updated><id>https://imageslr.com/feed.xml</id><title type="html">Images’ Blog</title><subtitle>Software | Elon Z
</subtitle><author><name>Images。</name></author><entry><title type="html">📺【监控】研发基本功 - Grafana 使用指南 🆕</title><link href="https://imageslr.com/2024/grafana.html" rel="alternate" type="text/html" title="📺【监控】研发基本功 - Grafana 使用指南 🆕" /><published>2024-04-05T18:00:00+08:00</published><updated>2024-04-05T18:00:00+08:00</updated><id>https://imageslr.com/2024/grafana</id><content type="html" xml:base="https://imageslr.com/2024/grafana.html"><![CDATA[<h2 id="〇前言">〇、前言</h2>

<p>Grafana 是一个开源的数据可视化和监控平台。它提供了一个灵活且强大的界面，可以连接到各种不同类型的数据源，将其中的数据以图表的形式进行展示和分析。</p>

<p>Grafana 的灵活性和可扩展性使其成为一个强有力的<strong>服务运维工具</strong>和<strong>信息获取工具</strong>：</p>
<ul>
  <li>服务运维：展示服务的工程指标，如 CPU 利用率、吞吐、错误率等。</li>
  <li>信息获取：展示服务的内部状态，如实时在线人数、订单数等。</li>
</ul>

<p>配置好 Grafana 看板，可以提升问题排查的效率：直接在 Grafana 查看各类数据，无需跳转到数据源；也可以发现事件之间的关联，例如“CPU 利用率变高，是因为请求量涨了”。</p>

<p>本文围绕 Grafana 在后端开发中的高频使用场景，分享了 Grafana 的基础概念、可视化、高级功能等。目标是让读者知道 Grafana 有什么功能，先留下印象，然后在需要配置看板时随时查阅。</p>

<p><strong>学习 Grafana 的最好方式是亲自上手操作</strong>。本文使用 Grafana 官方网站提供的<a href="https://play.grafana.org/d/000000012/">沙盒环境</a>做演示。Grafana 的沙盒环境提供了一个<a href="https://grafana.com/docs/grafana/latest/datasources/testdata/">测试数据源</a>，可以声明式地生成随机时序数据，用于调试看板的功能。</p>

<div class="ant-alert ant-alert">
  <p class="mb-0">💡 演示版本：Grafana v10.3.0</p>
  <ul>
    <li>Grafana Sandbox：<a href="https://play.grafana.org/d/000000012/">https://play.grafana.org/d/000000012/</a></li>
    <li>Grafana 官网：<a href="https://grafana.com/grafana/">https://grafana.com/grafana/</a></li>
    <li>Grafana 官方提供的所有示例：<a href="https://play.grafana.org/dashboards">https://play.grafana.org/dashboards</a></li>
  </ul>

</div>

<p><strong>Grafana 界面总览</strong>：</p>

<p><img src="/media/grafana/image-20240121161253079.png" alt="image-20240121161253079" /></p>

<ul>
  <li>①：Dashboard 操作区 (添加行、添加 Panel、设置、保存)。</li>
  <li>②：时间范围选择区。</li>
  <li>③：自动刷新周期。</li>
  <li>④：变量选择区，见下文 <a href="#variables">Variables</a>。</li>
  <li>⑤：链接区，见下文 <a href="#links">Links</a>。</li>
  <li>⑥：Dashboard 内容区，包含多个 Rows，每个 Row 下面有多个 Panels。</li>
</ul>

<h2 id="一基础">一、基础</h2>

<h3 id="1-url--链接">1. URL / 链接</h3>

<p>一个 Grafana 看板的链接如下所示：</p>

<pre><code class="language-plaintext">https://play.grafana.org/d/000000012/grafana-play-home?orgId=1
</code></pre>

<p>从前往后依次：</p>

<ul>
  <li>
    <p><code>/d</code> 表示看板 (Dashboard)，相应的，如果是一个文件夹 (Folder)，那么这里是 <code>/f</code>：</p>

    <pre><code class="language-plaintext">https://play.grafana.org/dashboards/f/QQTPJnF4z/
</code></pre>
  </li>
  <li>
    <p><code>000000012</code>是这个看板的唯一<code>ID</code>，在整个系统中不能重复。</p>
  </li>
  <li>
    <p><code>grafana-play-home</code> 是这个看板的名称。如果是中文名称，会自动转成拼音。看板的名称可以随便改，也可以重复。通过 URL 的 <code>ID</code> 就能确定唯一的 Grafana 看板，名称只是用来展示的。</p>
  </li>
  <li>
    <p><code>?orgId=1</code> 是看板的参数部分。所有变量 / 参数会通过 <code>&amp;</code> 连接。</p>
  </li>
</ul>

<p>每次修改看板的<a href="#use-variable">变量值</a>，url 里就会增加形如 <code>var-foo=xxx&amp;var-bar=xxx</code> 的字符串，这是当前看板的<strong>所有变量</strong>的取值：</p>

<p><img src="/media/grafana/image-20240121150637119.png" alt="image-20240121150637119" /></p>

<p>每次修改看板右上角的时间范围，或者鼠标拖动框选一段时间轴，url 里就会增加形如 <code>from=xxx&amp;to=xxx</code> 的字符串，这是当前看板的<strong>时间戳</strong>范围：</p>

<pre><code class="language-plaintext">https://play.grafana.org/d/000000012/grafana-play-home?orgId=1&amp;from=1689073420099&amp;to=1689076407767
</code></pre>

<p class="ant-alert ant-alert-info">💡 如果你的目的是希望其他人在打开链接时能够还原现场，那么需要分享带有时间范围和变量取值的完整 URL。否则，只需要看板 ID 即可，比如 <a href="https://play.grafana.org/d/000000012">https://play.grafana.org/d/000000012</a>。</p>

<h3 id="2-json-model">2. JSON Model</h3>

<p>Grafana 的整个看板内容 —— 包括所有的设置项 —— 都是用 <strong>JSON</strong> 描述的。这意味着我们可以直接编辑 JSON 格式的字符串，来达到修改看板的目的。某些场景下，这比操作 UI 界面更方便。</p>

<p>可以在“Dashboard Settings - JSON Model”找到当前看板的 JSON：</p>

<p><img src="/media/grafana/image-20240121151125411.png" alt="image-20240121151125411" /></p>

<p>JSON Model 中的字段说明：</p>

<ul>
  <li><code>iteration</code>：在什么时间被修改。Grafana 通过这个字段来判断是否和其他人的修改发生冲突。</li>
  <li><code>id</code>、<code>uid</code>：看板的唯一标识，即 URL 中的 <code>ID</code> 部分。</li>
  <li>
    <p><code>links</code>：看板上面的链接</p>
  </li>
  <li><code>templating</code>：看板的变量 Variables</li>
  <li>
    <p><code>panels</code>：类型是数组，每个元素表示页面中的一个面板 (Panel，<code>type=graph</code>)，或者一行 (Row，<code>type=row</code>)。</p>
  </li>
  <li>所有字段说明：<a href="https://grafana.com/docs/grafana/latest/dashboards/json-model/">https://grafana.com/docs/grafana/latest/dashboards/json-model/</a></li>
</ul>

<p>JSON Model 的典型使用场景：</p>

<ul>
  <li>复制整个 Dashboard</li>
  <li>批量复制或修改多个 Panels</li>
  <li>批量替换所有 Panels 的 DataSource 或 Metrics 前缀</li>
  <li>批量为所有 Panels 注入 Query 对应的外部链接 (需要编写代码解析和修改 JSON Model)</li>
</ul>

<p class="ant-alert ant-alert-info">💡 可以直接在 VS Code 等编辑器里修改 JSON Model，也可以用 JavaScript 或 Python 代码修改。修改后的内容粘贴到设置页，保存即可生效。但要注意，以下几个字段必须使用原来的值，不能随意替换：<code>id</code>、<code>uid</code>、<code>iteration</code>。否则会报错：“Dashboard has been changed by someone else”。</p>

<h3 id="3-版本控制">3. 版本控制</h3>

<p>在“Dashboard Settings - Versions”可以看到最近的更改历史，可以回滚。</p>

<p><img src="/media/grafana/image-20240121160708350.png" alt="image-20240121160708350" /></p>

<h3 id="4-文件夹--权限控制">4. 文件夹 / 权限控制</h3>

<p>Grafana 的权限控制遵循 RBAC 策略。</p>

<p>Grafana 提供以下三种角色：</p>

<ul>
  <li>Viewer：可以查看仪表盘，但不能修改</li>
  <li>Editor：可以查看和修改仪表盘</li>
  <li>Admin：可以管理整个仪表盘、分配权限</li>
</ul>

<p>Grafana 的权限可以在以下两个层级配置：</p>

<ul>
  <li>仪表盘：一个 Dashboard 权限。</li>
  <li>文件夹：一个文件夹可以包含多个 Dashboard。拥有该文件夹权限的用户，会自动拥有文件下所有仪表盘的权限。</li>
</ul>

<p>在“Dashboard Settings - Permissions”可以修改他人权限。权限可以分配给个人或团队。</p>

<p>如果想创建一个”只读”的看板，只需要将 Editor 的权限从<code>Edit</code>改成<code>View</code>。</p>

<h3 id="5-复制-dashboard">5. 复制 Dashboard</h3>

<p class="ant-alert ant-alert-info">💡 为了方便练习和保存，可以先把 Grafana 官方沙盒看板复制一份。</p>

<p>(1) 如果有看板的编辑权限，进入看板的设置页，点击 <code>Save As...</code> 即可：
<img src="/media/grafana/image-20240121153633234.png" alt="image-20240121153633234" /></p>

<p>(2) 如果没有编辑权限，可以点击「Share - Export - View JSON」，复制 JSON Model。
<img src="/media/grafana/image-20240121153808991.png" alt="image-20240121153808991" /></p>

<p>接下来有两种导入方式：</p>

<p>① 方法一：”New Dashboard - Import → Import via dashboard JSON model”，粘贴 JSON Model 内容，Load。然后修改 Name 和 UID，否则会报错。</p>

<p><img src="/media/grafana/image-20240121154205507.png" alt="image-20240121154205507" width="500px" /></p>

<p><img src="/media/grafana/image-20240121154243996.png" alt="image-20240121154205507" width="500px" /></p>

<p>② 方法二：新建一个空白看板，进入该看板的「Dashboard Settings - JSON Model」，使用该看板的 <code>id</code>、<code>uid</code>、<code>title</code>、<code>iteration</code> 字段的值，合并到刚才复制的 JSON Model 中，整体粘贴覆盖，保存。</p>

<h3 id="6-复制-panel">6. 复制 Panel</h3>

<p><strong>如果有编辑权限</strong>：</p>

<ol>
  <li>
    <p>Panel 菜单 → More → Copy：</p>

    <p><img src="/media/grafana/image-20240121154733223.png" alt="image-20240121154733223" /></p>
  </li>
  <li>
    <p>Add panel → Paste panel</p>

    <p><img src="/media/grafana/image-20240121155038233.png" alt="image-20240121155038233" /></p>
  </li>
</ol>

<p><strong>如果没有编辑权限</strong>：</p>

<ol>
  <li>
    <p>Panel 菜单 → Inspect → Panel JSON：
<img src="/media/grafana/image-20240121154904497.png" alt="image-20240121154904497" /></p>
  </li>
  <li>
    <p>将这个 Panel JSON 复制到目标 Dashboard 的 JSON Model - Panels 中。或者参考这个 Panel JSON，手动配置一个一样的 Panel。</p>
  </li>
</ol>

<h3 id="default">7. 保存看板默认状态</h3>

<p>看板的所有状态都可以被保存。包括：</p>

<ul>
  <li>当前选择的时间范围</li>
  <li>当前各个变量的值</li>
  <li>当前每一行是展开还是折叠的</li>
  <li>右上角的自动刷新周期</li>
</ul>

<p>建议：</p>

<ol>
  <li>建议设置好看板的默认状态，比如：选择最近 3 小时、选择默认机房、展开核心指标行、其他行默认折叠。</li>
  <li>
    <p>点击 Save，勾选以下两项，保存看板的默认状态：</p>

    <p><img src="/media/grafana/image-20240121160835012.png" alt="image-20240121160835012" /></p>
  </li>
  <li>分享仅带有 <code>/d/{unique_id}</code> 的简单 URL，比如 <a href="https://play.grafana.org/d/000000012">https://play.grafana.org/d/000000012</a></li>
</ol>

<h2 id="variables">二、变量 Variables</h2>

<p>变量 (Variables) 是 Grafana 的一项强大功能，可以用于创建动态的、可配置的、模板化的仪表盘。比如创建一个通用大盘，监控多个服务，而无需为每个服务创建单独的看板。</p>

<h3 id="1-配置变量">1. 配置变量</h3>

<h4 id="变量类型">变量类型</h4>

<p>变量可以在“Dashboard Settings - Variables”配置。</p>

<p><img src="/media/grafana/image-20240121225242430.png" alt="image-20240121225242430" /></p>

<p>Grafana 提供了多种变量类型：</p>

<ol>
  <li>Query：使用数据源的查询语言，动态获取可选项。</li>
  <li>Custom：手动定义一组可选项，是一组用逗号分隔的值列表，这些值将作为下拉菜单的选项。</li>
  <li>Textbox：添加一个文本框，用户可以输入任意文本。</li>
  <li>Constant：常量，在仪表盘的变量区域不可见。</li>
  <li>Interval：定义一组时间间隔，可以用于更改仪表板上的时间范围或聚合级别。</li>
  <li>Data source：这种类型的变量在有多个数据源时特别有用，允许用户动态切换数据源。</li>
  <li>Ad-hoc filters：动态添加、修改或删除过滤条件，仅支持某些数据源，如 ES、InfluxDB。</li>
</ol>

<p>下面将依次介绍使用频率最高的变量类型：<code>Custom</code>、<code>Textbox</code>、<code>Query</code>。</p>

<h5 id="custom">Custom</h5>

<p><img src="/media/grafana/image-20240121230253110.png" alt="image-20240121230253110" /></p>

<ul>
  <li>
    <p>①：提供几个固定的选项，逗号分隔。</p>
  </li>
  <li>
    <p>②：默认单选，可以支持多选。</p>
  </li>
  <li>
    <p>③：当允许多选时，可以有一个“全选”的选项。</p>
  </li>
  <li>
    <p>④：“全选”的默认值是所有值拼起来，如<code>{value1, value2, ...}</code>。 可以自定义一个值，如<code>*</code>。</p>
  </li>
  <li>
    <p>⑤：Custom 变量的选择框也是输入框，可以临时输入一个不存在于固定选项中的值，如下图。</p>
  </li>
</ul>

<p><img src="/media/grafana/image-20240121230355503.png" alt="image-20240121230355503" style=";" /></p>

<h5 id="textbox">Textbox</h5>

<p>简化版的 Custom。就是一个输入框，可以输入任意值。</p>

<p><img src="/media/grafana/image-20240121230508338.png" alt="image-20240121230508338" /></p>

<h5 id="query">Query</h5>

<p>Custom 类型的变量只能提供固定的值列表，而 Query 类型的变量可以<strong>实时查询</strong>某个 metrics name 下的某个 tag 的取值。典型的应用场景是“获取服务的所有上游 / 下游”。</p>

<p>下面以 OpenTSDB 数据源为例，演示 Query 类型变量的使用方式：</p>

<p><img src="/media/grafana/image-20240121230725233.png" alt="image-20240121230725233" /></p>

<ul>
  <li>①：这里是 OpenTSDB 提供的查询语法，详见<a href="http://opentsdb.net/docs/build/html/user_guide/query/examples.html">官网</a>，作用是获取 Metrics 打点 <code>throughput</code> 下的 <code>from</code> tag 的所有值。其时间范围默认是最近一个小时。</li>
  <li>②：建议将选项列表设置为<strong>字母顺序排列</strong>，方便查找和选择</li>
  <li>③：如果 Query 中用到了别的变量，需要将<code>Refresh</code>设置为<code>On Dashboard Load</code>或者<code>On Time Range Change</code>。</li>
</ul>

<p>Query Options 中的“<strong>Regex</strong>”可以用来过滤字段。比如只保留 <code>test_</code> 开头的值：</p>

<pre><code>/^test_/
</code></pre>

<p>另外，用正则的捕获组，可以把 <code>test_</code> 前缀去掉，只保留后面的内容。典型的使用场景是：Query 返回了 <code>test_foo</code>和<code>test_bar</code>，但需要提取其中的 <code>foo</code> 和 <code>bar</code> 用在 Panel 中：</p>

<pre><code>/((?&lt;=test_)*)/
</code></pre>

<blockquote>
  <p><a href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex/">https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex/</a></p>
</blockquote>

<p class="ant-alert ant-alert-info">💡建议：<strong>能用 Query 尽量不要用 Custom</strong>，这样能保证看板的通用性。</p>

<p class="ant-alert ant-alert-info">💡 在配置 Panel 时，经常会出现<code>foo=*</code>这样的语法，用来枚举<code>foo</code>的所有可能取值。这种情况下，建议将<code>foo</code>配置为一个 Query 变量，使用<code>foo=${foo}</code>。不仅默认效果一样，必要时还可以按值过滤下钻，逐步定位问题。</p>

<h4 id="变量默认值">变量默认值</h4>

<p>如 <a href="#default">1.7-保存看板默认状态</a> 所述：</p>

<ul>
  <li>默认情况下，当选择了某个变量后，变量的值会附在 URL 里，以 <code>var-xxx</code> 开头：
    <pre><code class="language-plaintext">  https://play.grafana.org/d/000000012/grafana-play-home-copy-2?orgId=1&amp;var-query0=123&amp;from=1705823477666&amp;to=1705824050985&amp;var-foo=1233&amp;var-bar=213&amp;var-custom=1&amp;var-custom=2&amp;var-from=222
</code></pre>
  </li>
  <li>
    <p>保存看板时，会提示是否要保存当前选择的变量值作为默认值：</p>

    <p><img src="/media/grafana/image-20240121160835012.png" alt="image-20240121160835012" /></p>
  </li>
  <li>保存默认值后，访问不携带任何参数的看板链接：<a href="https://play.grafana.org/d/000000012">https://play.grafana.org/d/000000012</a>，就会自动加载变量的默认值。</li>
</ul>

<p>因此，建议保存变量的默认值。一般来说，默认值都是 <code>All</code>，然后让用户自己过滤。某些变量可以设置成一个主要的值，避免面板上曲线太多，对用户产生干扰。</p>

<h3 id="use-variable">2. 使用变量</h3>

<h4 id="基本使用">基本使用</h4>

<p>使用变量的语法是<strong><code>$varname</code></strong>或<strong><code>${varname}</code></strong>。推荐使用后一种，因为在某些场景下，grafana 无法正确区分变量名的边界，比如把<code>$service.xxx.xxx</code>识别成一个变量，但实际上应该是<code>$service</code>。</p>

<p>在任何地方都可以直接使用变量：</p>

<ul>
  <li>标题</li>
  <li>链接</li>
  <li>Query</li>
  <li>Alias</li>
  <li>Tags</li>
  <li>…</li>
</ul>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/000000056/">Templated dynamic dashboard</a>。基于变量实现了一个动态看板，变量的值来自 Graphite Query。</p>

<h4 id="变量修饰符">变量修饰符</h4>

<p>变量会被替换为一个字符串。当某个变量可以多选时，Granafa 默认会生成<code>{foo,bar}</code>形式的字符串。可以通过修饰符控制生成的字符串形式。</p>

<p>常用的修饰符：</p>

<ul>
  <li><code>${var:pipe}</code>：生成<code>foo|bar</code>。这个是 OpenTSDB tags 能够识别的语法，因此建议 tags 中所有变量值都写成<code>:pipe</code>的形式。</li>
  <li><code>${var:queryparam}</code>：生成<code>var=foo&amp;var=bar</code>。如果希望通过 URL 传递变量，需要这样写。</li>
  <li>所有修饰符见 <a href="https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options/">Grafana 文档</a>。</li>
</ul>

<div class="ant-alert ant-alert-info">
  <p>💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1">Template VariablesFormatting Options</a>。修改 servers 变量，查看不同修饰符的渲染结果。
<img src="/media/grafana/image-20230722221428085.png" alt="image-20230722221428085" /></p>
</div>

<h4 id="内置变量--全局变量">内置变量 / 全局变量</h4>

<p>Grafana 内置了一些全局变量，比如时间范围<code>$__from</code>、<code>$__to</code>，或者<code>$__all_variables</code> (所有变量的当前取值，表示为 url query parameters 形式) 等。详见 <a href="https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/">Grafana 文档</a>。</p>

<p>全局变量也支持修饰符。<code>$__from</code>和<code>$__to</code>还支持如下的日期格式化语法：</p>

<p><img src="/media/grafana/image-20240121232653652.png" alt="image-20240121232653652" /></p>

<p class="ant-alert ant-alert-info">💡典型使用场景：配置了跳转到其他看板的链接，希望附带当前看板的所有状态 (变量值、时间范围等)。Grafana 官方 Demo：<a href="https://play.grafana.org/d/HYaGDGIMk/templating-global-variables-and-interpolation?orgId=1">内置全局变量</a>。</p>

<h3 id="3-变量联动--键值对映射">3. 变量联动 / 键值对映射</h3>

<p>变量联动是生产环境中的常见需求，但当前 Grafana 没法很好支持。一个典型的场景是根据地区选择相应的服务器：</p>

<pre><code class="language-Plaintext">   A    |  B 
------- | -------
cn      | shanghai
us      | new_york, silicon
</code></pre>

<p>解决方法是：</p>

<ul>
  <li>A 配置成一个 Custom 变量，值是 <code>cn, us</code></li>
  <li>B 配置成一个 Query 类型的变量，值形如 <code>Query($A)</code></li>
</ul>

<p>B 的具体语法依赖于选择的数据源。可以把 A → B 的映射存到 Prometheus 等数据库来筛选，但这样比较麻烦。更简单的方法是实现一个类似 <a href="httpbin.org">httpbin.org</a> 这样的 <em>Echo 服务</em>，并注册为一个数据源。这种 Echo 服务会将传递过来的参数原封不动地返回，比如访问 <a href="https://httpbin.org/get?cn=shanghai&amp;us=new_york&amp;us=silicon">https://httpbin.org/get?cn=shanghai&amp;us=new_york&amp;us=silicon</a> 会得到以下结果：</p>

<pre><code class="language-json">{
  "args": {
    "cn": "shanghai", 
    "us": [
      "new_york", 
      "silicon"
    ]
  }
}
</code></pre>

<p>然后使用 <code>jsonPath: $.args[$A]</code> 就能获取到 A 对应的 B 的值了。</p>

<blockquote>
  <p>具体实现略，欢迎补充。如果有需求，请联系公司的 Grafana 管理员。</p>
</blockquote>

<h3 id="variable-repeat">4. 变量循环 / Repeated</h3>

<p>Grafana 提供了 Repeated rows 和 Repeated Panels 功能，可以根据变量的值动态复制行或面板的布局。</p>

<p>以行重复为例。在行标题旁边点击齿轮图标，打开“Row Options”，可以看到一个名为 “Repeat for” 的选项。在这里选择要按哪个变量重复，然后保存。</p>

<p><img src="/media/grafana/image-20240121235350674.png" alt="image-20240121235350674" /></p>

<p>然后这一行便会按照该变量的当前取值重复多次，行内的所有面板均会被复制。在重复行内，访问重复变量名 <code>$custom</code> 将会获取到单个值，而不是所有值，如下图所示。</p>

<p><img src="/media/grafana/image-20240121235543708.png" alt="image-20240121235543708" /></p>

<p><img src="/media/grafana/image-20240121235615376.png" alt="image-20240121235615376" /></p>

<div class="ant-alert ant-alert-info">
  <p class="pb-0 mb-0">💡 Grafana 官方的演示看板：</p>

  <ul>
    <li><a href="https://play.grafana.org/d/000000153/repeat-rows?orgId=1">Repeat Rows</a>、<a href="https://play.grafana.org/d/k3PEoCpnk/repeating-a-row-with-a-non-repeating-panel-and-horizontal-repeating-panel?orgId=1">水平重复</a>、<a href="https://play.grafana.org/d/7lS-ojt7z/repeating-a-row-with-a-non-repeating-panel-and-vertical-repeating-panel?orgId=1">垂直重复</a></li>
    <li><a href="https://play.grafana.org/d/000000025">Repeat Panels</a>、<a href="https://play.grafana.org/d/WVpf2jp7z/repeating-a-panel-horizontally?orgId=1">水平重复</a>、<a href="https://play.grafana.org/d/OY8Ghjt7k/repeating-a-panel-vertically?orgId=1">垂直重复</a></li>
  </ul>
</div>

<h2 id="三数据源-data-sources">三、数据源 Data Sources</h2>

<p>在配置 Grafana 看板时，需要在“Query”区域选择一个数据源：</p>

<p><img src="/media/grafana/image-20240405225426753.png" alt="image-20240405225426753" /></p>

<p>Grafana 有两种常见的数据源：<a href="http://opentsdb.net/">OpenTSDB</a> 和 <a href="https://bosun.org/">Bosun</a>。Grafana 的数据源需要在管理员后台配置，这里我们假设读者了解这两个数据源、且公司已经在 Grafana 系统中配置好了这两个数据源。接下来介绍这些数据源的使用方法。</p>

<p>Grafana Play Ground 还提供了一个<a href="https://grafana.com/docs/grafana/latest/datasources/testdata/">测试数据源</a>，可以声明式地生成随机时序数据。在后面的“可视化”章节中，我们会使用这个测试数据源生成示例数据。</p>

<h3 id="1-opentsdb">1. OpenTSDB</h3>

<p><img src="/media/grafana/opentsdb_query_editor.png" alt="img" /></p>

<ul>
  <li>Aggregator (第一行)：聚合方式，avg 或 sum。</li>
  <li>
    <p>Alias：提供一个可读的别名。常见的使用方式是配合“Filters / Tags”，比如有个 tag key 的名称为 <code>cluster</code>，alias 就可以配置为 <code>cluster=$tag_cluster</code>，当有  <code>default</code>、<code>test</code> 两个 cluster 时，会显示 <code>cluster=default</code>、<code>cluster=test</code>。</p>

    <p><img src="/media/grafana/image-20240405230738145.png" alt="image-20240405230738145" /></p>
  </li>
  <li>Filters / Tags：根据 tags key=value 过滤。这两个是互斥的，配一个就可以。推荐用 Filters。
    <ul>
      <li>literal_or：包含，支持 <code>.</code>、<code>*</code> 通配符</li>
      <li>not_literal_or：不包含</li>
      <li>regexp：POSIX 兼容格式的正则表达式</li>
      <li>group_by (仅 Filters 有此选项，Tags 总是 true)：如果勾选上，那么同名的 tag_value 会聚合成一条曲线</li>
    </ul>
  </li>
  <li>Rate：对应 OpenTSDB 的 rate_counter 类型的打点。选择后会出现“Counter”选项。</li>
  <li>TopK：仅保留若干个最高/最低值，适用于按 Tag 分组后曲线过多的场景。比如展示所有主机的 CPU 利用率时，可以仅展示 top 10 和 bottom 10。</li>
</ul>

<p class="ant-alert ant-alert-info">💡  关于 OpenTSDB 数据源的更多配置说明，详见 <a href="https://grafana.com/docs/grafana/latest/datasources/opentsdb/">Grafana 官方文档</a>。</p>

<h3 id="2-bosun">2. Bosun</h3>

<p>所有 OpenTSDB 类型的都可以写成 bosun 的形式。举个例子，假设我们有一个OpenTSDB 查询，它使用以下参数：</p>

<ul>
  <li>metric：system.cpu.usage</li>
  <li>aggregator：sum</li>
  <li>tags：host=*</li>
  <li>downsample：5m-avg</li>
</ul>

<p>将这个 OpenTSDB 查询转换为 Bosun 表达式后如下所示：</p>

<pre><code class="language-text">$q("sum:5m-avg:system.cpu.usage{host=*}")
</code></pre>

<p>但是 bosun 的可读性不高，除了以下场景外，<strong>不建议使用 Bosun，尽量使用 OpenTSDB</strong>：</p>

<ul>
  <li>
    <p>计算错误率。使用 Bosun 可以表示两个时序打点的除法：</p>

    <pre><code class="language-text"> $succ = q("sum:rate{counter}:success.throughput", "$start", "")
 $error = q("sum:rate{counter}:error.throughput", "$start", "")
 $error / ($succ + $error)
</code></pre>
  </li>
</ul>

<p class="ant-alert ant-alert-info">💡  关于 Bosun 语法的更多说明，详见 <a href="https://bosun.org/">Bosun 官方文档</a>。</p>

<h3 id="3-grafana-test-data">3. Grafana Test Data</h3>

<p><a href="https://grafana.com/docs/grafana/latest/datasources/testdata/">TestData data source</a> 是 Grafana 官方提供的一个测试数据源，用于生成模拟的时序数据，非常适合用来测试看板的功能。Grafana 的沙盒环境内置了这个数据源，我们在第五节“可视化”章节中会使用到。</p>

<p><img src="/media/grafana/screenshot-testdata-add-10.0.png" alt="img" /></p>

<ul>
  <li>
    <p>Random Walk：随机数，可以指定 Series num</p>
  </li>
  <li>
    <p>Slow Query：指定返回数据的耗时</p>
  </li>
  <li>
    <p>CSV Context：自己粘贴一个 CSV 数据，比如：</p>

    <pre><code class="language-plaintext">Name,Value,Unit,Color
Temperature,10,degree,green
Pressure,100,bar,blue
Speed,30,km/h,red
</code></pre>
  </li>
</ul>

<h2 id="四界面-rows--panels">四、界面 Rows / Panels</h2>

<p>这一节介绍了“行”和“面板”的基础操作和配置。</p>

<h3 id="1-行-rows">1. 行 Rows</h3>

<p>在 Dashboard 的右上角添加行：</p>

<p><img src="/media/grafana/image-20240405232212269.png" alt="image-20240405232212269" /></p>

<p>当行折叠起来时，最右边会有一个 Handler (下图①)，点击拖动可以调整该行的位置：</p>

<p><img src="/media/grafana/image-20240405232114641.png" alt="image-20240405232114641" /></p>

<p>鼠标移动到行标题上，会出现一个齿轮和删除按钮 (上图②)。点击齿轮，可以修改行的标题，或配置按变量重复行 (<a href="#variable-repeat">见 2.4</a>)。</p>

<h3 id="2-面板-panels">2. 面板 Panels</h3>

<p>面板 (Panel) 右上角的菜单提供了以下功能：</p>

<p><img src="/media/grafana/image-20240405232533826.png" alt="image-20240405232533826" /></p>

<ul>
  <li>View：在整个网页窗口里，放大查看这个面板</li>
  <li>Edit：编辑面板，高频使用</li>
  <li>Share：分享该面板的链接</li>
  <li>Explort：基于该面板配置的 Query，调整查询参数、时间范围或可视化形式</li>
  <li>Inspect：查看该面板的 Data 和 JSON Model (在没有编辑权限的时候，可以通过 JSON 查看面板配置的 Metrics 名称是什么)</li>
</ul>

<p>鼠标移动到面板的标题区域，鼠标指针会变成一个十字，拖动可以移动面板。</p>

<h3 id="readability">3. 提升面板的可读性</h3>

<p>点击“Edit”进入面板的编辑页，右边提供了一系列配置项：</p>

<p><img src="/media/grafana/image-20240405233608653.png" alt="image-20240405233608653" /></p>

<p>强调以下几个功能，提高面板的可读性和信息量，使其更易用：</p>

<ul>
  <li><strong>标题</strong>：面板的标题需要和 metrics 含义一致
    <ul>
      <li><code>cpu.utilization</code> → <code>CPU 利用率</code></li>
      <li><code>xxx.calledby.success</code> → <code>XXX 接口成功 QPS</code></li>
    </ul>
  </li>
  <li>
    <p><strong>描述</strong>：为看板补充必要的、更详细的描述信息，用户将鼠标移动到图标上时会展示
<img src="/media/grafana/image-20240405234150418.png" alt="image-20240405234150418" /></p>
  </li>
  <li>
    <p><strong>Tooltip 降序排列</strong>：和变量要按字母序排列一样，鼠标浮动到面板上展示的 Tooltip 要降序排列，降序排列后刚好和所有曲线从上到下的顺序一致。Panel Settings - Display - Hover tooltip - Sort Order - <strong>Decreasing</strong> (不同版本的 Grafana，配置项的路径可能有区别)</p>
  </li>
  <li>
    <p><strong>Legend 按表格展示</strong>，适用于曲线分组较多的场景：Legend - 勾选“Show As Table” + “Max / Avg / Current”，按 avg 降序排列 (不同版本的 Grafana，配置项的路径或名称可能有区别，比如下图最新版的 Grafana 中 <code>avg</code> 被替换成了 <code>Mean</code>)</p>

    <p><img src="/media/grafana/image-20240405235749133.png" alt="image-20240405235749133" /></p>
    <ul>
      <li>Show As Table：按表格展示，可以方便地升序或降序排列。</li>
      <li>Avg (Mean)：了解这段时间范围的整体情况，推荐设置为默认排序方式。</li>
      <li>Max：有时候有用，比如想查看晚高峰的 CPU 利用率峰值。</li>
      <li>Current (Last)：看实时变化，比如错误数有没有下降。</li>
    </ul>
  </li>
  <li><strong>设置合适的单位：</strong>Panel Settings - Axes - Unit。常用的场景：
    <ul>
      <li>QPS：Misc - short</li>
      <li>利用率：Misc - Percent (0.0~1.0)</li>
      <li>带宽：Data - bytes</li>
      <li>延迟：Time - microseconds (us)</li>
    </ul>
  </li>
  <li><strong>Alias</strong>：应当包含尽可能多的信息量。
    <ul>
      <li>默认的曲线名称是根据 Query 配置自动生成，形如 <code>metrics_name{key1=value1, key2=value2}</code>，比如 X 调用 Y 服务的 <code>foo</code> 接口，名称默认是 <code>throughput{from=X, to=Y, api=foo}</code>，可读性很差。</li>
      <li>建议配置一个更直观的 Alias，比如上面的例子，可以改为 <code>$tag_from → $tag_to::$tag_api()</code>，展示出来形如 <code>X → Y::foo()</code>，能直观看出打点的含义。</li>
    </ul>
  </li>
  <li>
    <p><strong>配置和面板 Query 含义一致的链接</strong>：用户发现某个面板的数据有异常后，经常需要基于该面板的 Query，做更进一步的查询。这里可以用 Grafana 面板自带的 Explore 功能，但更多时候用户会跳转到另一个平台，比如 OpenTSDB 数据源总是有一个配套的 Metrics 平台、数据库总是有一个 SQL 平台… 可以将 Query 对应的平台链接附在 Panel 上，用户就可以在左上角“描述”区域直接点击链接跳转。Panel Settings - Links - Add。</p>

    <p><img src="/media/grafana/image-20240406000638035.png" alt="image-20240406000638035" /></p>

    <ul>
      <li>URL 里可以使用变量。常见的是将 Query 用到的变量和 Grafana 当前选择的时间范围附在 URL 里传递过去，这样用户点开链接后，看到的是和 Grafana 面板一模一样的数据，能直接配置其他筛选条件。</li>
      <li>勾选“Open in new tab”</li>
    </ul>
  </li>
</ul>

<div class="ant-alert ant-alert-info">

  <p class="pb-0 mb-0">💡 面板的可读性越高，排查问题的效率越高：</p>

  <ul>
    <li>可读性低的表现：总是需要编辑面板，查看 metrics 名称和 tags 是什么。</li>
    <li>可读性高的表现：通过面板标题和曲线名称，就可以直观地获取这些信息。</li>
  </ul>

</div>

<h2 id="五可视化-visualizations">五、可视化 Visualizations</h2>

<p>这一小节会介绍 Grafana 的几种可视化形式及其配置项。建议在 Grafana 沙盒里编辑测试，会更直观。如果要获取看板设计的灵感，可以参考附录中的 Grafana Demo。</p>

<h3 id="graph-折线图">Graph 折线图</h3>

<p>90% 以上的场景，用折线图就够了：</p>

<p><img src="/media/grafana/image-20240406001337537.png" alt="image-20240406001337537" /></p>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/000000016/1-time-series-graphs?orgId=1">① Time Series 时序图（折线图）总览</a>、<a href="https://play.grafana.org/d/hxne1Hm4z/1-time-series-features-detailed-overview?orgId=1">② 每个配置项的细节</a></p>

<p>以 Grafana 最新版沙盒为例，面板编辑页的常用配置项 (从上到下)：</p>

<ul>
  <li>
    <p>Alias、Tooltip 降序排列、Legend Show As Table：略，见上文</p>
  </li>
  <li>Axis：
    <ul>
      <li>
        <p>Placement：坐标轴放在哪里，默认靠左</p>
      </li>
      <li>
        <p>按对数比例展示：<a href="https://play.grafana.org/d/000000040/logarithmic-scales?orgId=1&amp;editPanel=1">Demo - 对数 Scale</a></p>
      </li>
    </ul>
  </li>
  <li>Graph styles：
    <ul>
      <li>Lines、Bars、Points：显示为折线图、柱状图还是散点图</li>
      <li>Line width：线宽</li>
      <li>Fill opacity：折线图是否要有填充，建议设置为 0</li>
      <li>Line style：线条样式，实线 or 虚线 or 点状，适用于同比视图中，区分今天和昨天的曲线</li>
      <li>Show points：是否展示每个数据点 (默认只展示折线)</li>
    </ul>
  </li>
  <li>Standard options - Unit：建议配置合理的单位，以提升看板的可读性
    <ul>
      <li>QPS：Misc - short</li>
      <li>利用率：Misc - Percent (0.0~1.0)</li>
      <li>带宽：Data - bytes</li>
      <li>延迟：Time - microseconds (us)
override</li>
    </ul>
  </li>
  <li>
    <p>Data links：添加链接，详见 <a href="#readability">4.3-提升面板的可读性</a></p>
  </li>
  <li>
    <p>Value mappings：按条件将某个值映射为其他值，比如将 <code>P0</code>、<code>P1</code>  映射为<code>核心</code>、<code>非核心</code></p>
  </li>
  <li>
    <p>Thresholds：添加一条阈值线 (或填充区域)。可以配置阈值的颜色和值，默认<strong>大于</strong>阈值的区域会被填充颜色，如果想表达“低于阈值”时是异常情况 (比如服务 SLO 指标)，可以替换下图中 Base 和 80 的颜色。</p>

    <p><img src="/media/grafana/image-20240406004127227.png" alt="image-20240406004127227" /></p>
  </li>
  <li>Series Override：一个很有用的功能，按照正则表达式匹配曲线名称，然后修改这些曲线的属性。常见的使用场景是：(1) 为某些特殊的曲线指定颜色，比如总是高亮核心服务；(2) 将名称中包含 <code>test</code> 的曲线设置为虚线，表示这些是测试数据；(3) 在一个面板中画两条曲线，其中一条曲线的纵轴在左侧，另一条曲线的纵轴在右侧 (修改 Y-Axes)。</li>
</ul>

<h3 id="pie-饼图">Pie 饼图</h3>

<p>饼图适合表示各项数据的占比：</p>

<p><img src="/media/grafana/image-20240406010555305.png" alt="image-20240406010555305" /></p>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/ktMs4D6Mk/5-bar-charts-and-pie-charts?orgId=1">饼图和柱状图</a>、<a href="https://play.grafana.org/d/-Kj3rZdGz/piechart?orgId=1">Piechart 饼图</a></p>

<p>在旧版本的 Grafana 中，提供了一个“Combine (only for percentages)”配置项，可以将占比低于指定阈值的数据都聚合成”Others”，这样饼图就不会出现很多占比非常小的区域了。</p>

<h3 id="bar-直方图--柱状图">Bar 直方图 / 柱状图</h3>

<p><img src="/media/grafana/image-20240406010043454.png" alt="image-20240406010043454" /></p>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/ktMs4D6Mk/5-bar-charts-and-pie-charts?orgId=1">饼图和柱状图</a></p>

<ul>
  <li>Stacking：是否要将所有直方图堆叠起来。适合展示一些关心每个时刻总和的指标，比如当前有多少台机器 Coredump 了。</li>
</ul>

<h3 id="gauge-水位线">Gauge 水位线</h3>

<p>Gauge 适合展示总体水位，比如带宽是否满了，或者服务稳定性是否跌破阈值。</p>

<p><img src="/media/grafana/image-20230722231032773.png" alt="image-20230722231032773" /></p>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/KIhkVD6Gk/4-gauges?orgId=1&amp;refresh=10s">Gauge</a>、<a href="https://play.grafana.org/d/vmie2cmWz/bar-gauge?orgId=1&amp;refresh=10s">Bar Gauge</a></p>

<h3 id="stat-统计">Stat 统计</h3>

<p>突出显示当前时刻的值，可以在底部以阴影方式显示这段时间的曲线。</p>

<p><img src="/media/grafana/image-20230722230754838.png" alt="image-20230722230754838" /></p>

<p class="ant-alert ant-alert-info">💡 Grafana 官方的演示看板：<a href="https://play.grafana.org/d/Zb3f4veGk/2-stats?orgId=1">Stat 统计</a></p>

<h3 id="text--markdown--html-文本">Text / Markdown / HTML 文本</h3>

<p>Text 类型的面板支持写 Markdown 或者 HTML。</p>

<p>Markdown 适合写看板的使用说明：</p>

<p><img src="/media/grafana/image-20240406011449181.png" alt="image-20240406011449181" /></p>

<p>HTML 可以写更复杂的内容，比如把另一个 Grafana 内嵌到当前 Grafana：</p>

<pre><code class="language-HTML">&lt;iframe 
src="https://{grafana链接}?kiosk=tv&amp;${__url_time_range}" 
width="100%" height="100%" frameborder="0"&gt;&lt;/iframe&gt;
</code></pre>

<p>上面 <code>src</code> 里的 <code>kiosk=tv</code> 改成 <code>kiosk</code>，被嵌入的看板就没有标题栏、变量栏、Links 了，融合度更好。</p>

<h2 id="links">六、链接 Links</h2>

<p>Grafana Dashboard 顶部可以展示链接，通常会在这里附加其他看板和相关文档的链接。</p>

<p><img src="/media/grafana/image-20240406132300574.png" alt="image-20240406132300574" /></p>

<p>添加链接：Settings → Links → New link，点击右边的 ↑ ↓ 箭头可以调整顺序。</p>

<p><img src="/media/grafana/image-20240406132130917.png" alt="image-20240406132130917" /></p>

<p>配置项：</p>

<p><img src="/media/grafana/image-20240406132127748.png" alt="image-20240406132127748" /></p>

<ul>
  <li>链接标题</li>
  <li>链接类型：Dashboards 或 Link，前者是罗列当前系统里的所有看板，后者是自定义链接。</li>
  <li>URL：链接地址。这里可以传递看板里的<a href="#use-variable">变量</a>，比如配置了 <code>xxx.com?${foo:queryparam}</code>，如果当前看板的 <code>foo</code> 变量取值为 <code>123</code>，则会生成这样的链接：<code>xxx.com?foo=123</code>。</li>
  <li>Tooltip：鼠标移上去会有一个提示说明。</li>
  <li><code>include current time range</code>：点击链接跳转时，在 url 里传递当前看板选择的时间范围 <code>from=xxx&amp;to=xxx</code>。如果链接是另一个看板，则推荐勾选。</li>
  <li><code>include current template variable values</code>：点击链接跳转时，在 url 里传递当前看板选择的所有变量 <code>foo=xxx&amp;bar=xxx&amp;baz=xxx</code>。如果链接是另一个看板且变量配置是一致的，则推荐勾选。</li>
  <li><code>Open link in new tab</code>：在新标签页打开，推荐勾选。</li>
</ul>

<h2 id="七其他-others">七、其他 Others</h2>

<p><strong>Dashboard Settings：</strong></p>

<ul>
  <li>
    <p><strong>Auto refresh</strong>：配置不同的自动刷新间隔，在右上角选择，之后看板会每隔有单时间就自动刷新</p>

    <p><img src="/media/grafana/image-20240406140354809.png" alt="image-20240406140354809" /></p>

    <p><img src="/media/grafana/image-20240406140443330.png" alt="image-20240406140443330" /></p>
  </li>
  <li>
    <p><strong>Now delay</strong>：某些数据源的数据有延迟，最新时刻的数据可能不准，监控上会出现掉底。解决办法是配置 Now delay，总是丢弃掉最近 30s 的数据。</p>

    <p><img src="/media/grafana/image-20240406140723421.png" alt="image-20240406140723421" /></p>
  </li>
  <li>
    <p><strong>打开 Crosshair</strong>：Dashboard Settings - Graph tooltip，在不同 Panel 中同步显示当前鼠标所指的时刻。</p>

    <p><img src="/media/grafana/image-20240406140721056.png" alt="image-20240406140721056" /></p>
  </li>
</ul>

<hr />

<p><strong>交互：</strong></p>

<ul>
  <li>
    <p>在 Grafana Panel 里拖动可以选择一段时间范围，放大查看数据：</p>

    <p><img src="/media/grafana/image-20240406140559946.png" alt="image-20240406140559946" /></p>
  </li>
</ul>

<hr />

<ul>
  <li>
    <p>Grafana Panel 编辑页“Query”右边还有一个“Transform data”功能。通过 Transform 可以对 Query 结果相加、相除或合并等。典型的应用场景是“分别配置成功吞吐和失败吞吐的两个 Query，然后配置一个 Transform，计算错误率”，好处是相比于 Bosun 表达式可读性更高。</p>

    <p><img src="/media/grafana/image-20240406141137156.png" alt="image-20240406141137156" /></p>
  </li>
</ul>

<h2 id="附录">附录</h2>

<h3 id="grafana-官方示例看板">Grafana 官方示例看板</h3>

<p>Grafana 官方提供的所有示例：<a href="https://play.grafana.org/dashboards">https://play.grafana.org/dashboards</a>。</p>

<p>除了上文已经列出来的示例，这里收录了一些其他可能有用的看板：</p>

<ul>
  <li><a href="https://play.grafana.org/d/7p7JkqWVz/panel-tests-canvas-examples?orgId=1">Canvas 画布</a></li>
  <li>Table 表格：<a href="https://play.grafana.org/d/OhR1ID6Mk/3-table?orgId=1">First Demo</a>、<a href="https://play.grafana.org/d/U_bZIMRMk/table-panel-showcase?orgId=1">Second Demo</a>、<a href="https://play.grafana.org/d/T512JVH7z/loki-nginx-service-mesh-json-version?orgId=1&amp;var-datasource=LGTM%20Stack%20-%20Loki&amp;var-label_name=filename&amp;var-label_value=All&amp;var-job=All&amp;var-instance=All&amp;viewPanel=6">Third Demo</a></li>
  <li><a href="https://play.grafana.org/d/qD-rVv6Mz/6-state-timeline-and-status-history?orgId=1">State timeline, Status history</a></li>
  <li><a href="https://play.grafana.org/d/NpjdeVWGz/flowcharting-bug-draw-io-940?orgId=1">Flow chart 流程图</a>：嵌入 Draw.io
    <ul>
      <li><a href="https://play.grafana.org/d/ubByxW2Gz/flowcharting-dc-floor?orgId=1">Second Demo</a>、<a href="https://play.grafana.org/d/Kcic5xeWz/flowcharting-events-and-animations?orgId=1">Third Demo</a></li>
      <li><a href="https://play.grafana.org/d/JG78lp0Zk/flowcharting-expand-and-collapse?orgId=1">可以展开、折叠的 Demo</a></li>
      <li><a href="https://play.grafana.org/d/yNQz3OCZk/flowcharting-floorplan?orgId=1&amp;refresh=30s">鼠标浮在图片上，能看到对应的 Metrics 数据</a></li>
      <li><a href="https://play.grafana.org/d/p5F-FKCZk/flowcharting-technical-architecture?orgId=1">架构图</a></li>
    </ul>
  </li>
</ul>

<p>完整 Demo：</p>

<ul>
  <li>
    <p><a href="https://play.grafana.org/d/3SWXxreWk/grafana-dashboard?orgId=1">Grafana Dashboard</a>：折线图、柱状图、Bar Gauge。Graphite 数据集。</p>

    <p><img src="/media/grafana/image-20230722170943238.png" alt="image-20230722170943238" /></p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/cL5pLH7Wz/stats-overview?orgId=1">Stats Overview</a>：折线图、柱状图、Gauge 仪表盘、Bar Gauge、Stat。</p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/000000045/big-dashboard?orgId=1">Big Dashboard</a>：折线图、柱状图、堆叠、统计。Graphite 数据集。</p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/T512JVH7z/">Loki NGINX Service Mesh</a>：折线图、Stat、Gauge、Map、Log 日志、Table (内嵌 Gauge)。</p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/000000110/business-metrics?orgId=1">Business Metrics</a>：Stat 统计、阈值。</p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/d5dd0b9e-6c85-40dc-ad57-a2f703f3b92a/public-dashboards-multiple-panel-test-example?orgId=1">Multiple Panel Test Example</a>：所有类型的看板速览，包括 Text、Geomap、State timeline、Logs、Histogram、Heatmap、Gauge、Pie chart、Table、Time Series、Stat、Bar chart。</p>
  </li>
  <li>
    <p><a href="https://play.grafana.org/d/a87fb0d919ec0ea5f6543124e16c42a5/kubernetes-compute-resources-namespace-workloads?orgId=1&amp;refresh=30s">Kubernetes 服务资源利用率</a></p>
  </li>
</ul>

<h3 id="服务监控面板应该如何建设">服务监控面板应该如何建设</h3>

<p>一个服务级别的看板，应当包含这几行：</p>

<ul>
  <li>服务整体 (CPU、内存、吞吐、延时等核心指标)</li>
  <li>上游调用 (上游成功吞吐、失败吞吐、Error Code、错误率、延时 Avg、延时 P99)
    <ul>
      <li>
        <blockquote>
          <p>这里要在参数和 tags 里配置 from，区分不同上游；配置 method，区分不同接口。</p>
        </blockquote>
      </li>
    </ul>
  </li>
  <li>调用下游 (调用下游成功吞吐、失败吞吐、Error Code、错误率、延时 Avg、延时 P99 等)
    <ul>
      <li>
        <blockquote>
          <p>这里要在参数和 tags 里配置 to，区分不同下游。</p>
        </blockquote>
      </li>
    </ul>
  </li>
  <li>异常情况 (错误日志、Coredump、Panic 等)</li>
  <li>内部状态 (视服务而定，如候选条数、缓存大小等)</li>
  <li>分步骤延时</li>
  <li>单机视图 (CPU 利用率、内存利用率、Error QPS 等 Top 10 的 Hosts、Pods)</li>
  <li>分 Env 视图 (分线上环境、测试环境、小流量环境查看 CPU 利用率、内存利用率、Error QPS 等)</li>
  <li>…</li>
</ul>

<blockquote>
  <p>TODO：通用服务大盘，目前仅在字节内网可用</p>
</blockquote>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[〇、前言]]></summary></entry><entry><title type="html">☑️【2024】TODO LIST</title><link href="https://imageslr.com/2024/todo.html" rel="alternate" type="text/html" title="☑️【2024】TODO LIST" /><published>2024-01-01T12:00:00+08:00</published><updated>2024-01-01T12:00:00+08:00</updated><id>https://imageslr.com/2024/todo</id><content type="html" xml:base="https://imageslr.com/2024/todo.html"><![CDATA[<p>去年工作较忙，很少更新博客。新的一年，希望能保证至少一个季度一篇博客。对博客的要求依然是<strong>长期有价值</strong>。下面是想到的一些专题，后续再看有没有其他灵感：</p>

<ul>
  <li>《<strong>Grafana 使用指南</strong>》：Grafana 是一个开源的监控展示平台，也是后端开发人员排查问题的必备工具。虽然 Grafana 官方提供了详细的文档，但全英文 + 超长篇幅，阅读成本过高。这篇文章会围绕 <code>服务运维</code> 和 <code>信息获取</code> 中的高频使用场景，分享 Grafana 的基础概念 (JSON Model、Query、Variable)、可视化配置 (Panel、各类图表)、高级技巧 (变量联动、内嵌 HTML 页面)。同时，文章会讨论一个标准后端服务的稳定性看板应该如何建设，并提供一个覆盖全文技巧的看板示例。</li>
  <li>《<strong>如何写技术文档</strong>》：文档本身是一个信息传递工具，在多团队、多业务的工作模式下，写文档和写代码一样重要。然而大部分同学在写代码方面花了较多时间，但写文档比较随意，造成后续理解和沟通成本高。技术文档话题多样，相比于律师/财务等职业的文档，很难用统一的标准与框架来约束。因此，技术文档的好坏和文笔无关，和思维有关。这篇文章会围绕如何写技术文档，分享这些内容：
    <ul>
      <li>目标：让没有任何经验或上下文的读者能很快理解文章。</li>
      <li>思维：明确受众、规划结构、撰写内容。</li>
      <li>三趟式的写作流程：收集资料 (召回)、粗写、精写。</li>
      <li>其他技巧：样式规范、画图技巧、Markdown、快捷键、锚点</li>
    </ul>
  </li>
  <li>《<strong>工作三周年的感悟</strong>》：马上工作三周年了，工作方向调整了几次，也逐渐从实习生成长为模块负责人、校招生 Mentor、面试官。希望能做一次系统的总结，为自己的下一阶段做准备，也供其他同学参考。会覆盖这些话题：
    <ul>
      <li>做好工作：工具 (OKR、GTD)、思维 (分析问题、理解需求、提出方案；解决根本问题、通用化设计、面向长期)。</li>
      <li>团队合作：和同事沟通、和上级沟通、如何带新人。</li>
      <li>职业规划：定期复盘、短期和长期规划、里程碑。广度和深度。</li>
      <li>其他问题：判断某件工作的价值、判断自身的价值、精力与优先级分配。</li>
    </ul>
  </li>
  <li>…</li>
</ul>

<p>其他内容暂时没有想到。如果读者有建议，欢迎在评论区留言。</p>

<blockquote>
  <p>去年其实也围绕工作内容，在公司内网分享了几篇文章，包括研发基本功 (IDE、稳定性保障、服务看板建设、C++ 单元测试)、混排串讲、特征工程串讲等。但考虑到我的博客读者大部分是校招新人，这些和具体工作相关的内容就不在这里分享了。</p>
</blockquote>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[去年工作较忙，很少更新博客。新的一年，希望能保证至少一个季度一篇博客。对博客的要求依然是长期有价值。下面是想到的一些专题，后续再看有没有其他灵感：]]></summary></entry><entry><title type="html">🧑🏻‍💻【社招/校招/实习】字节跳动 Ads Infra 团队 🔥</title><link href="https://imageslr.com/2023/ads-infra.html" rel="alternate" type="text/html" title="🧑🏻‍💻【社招/校招/实习】字节跳动 Ads Infra 团队 🔥" /><published>2023-07-11T09:00:00+08:00</published><updated>2023-07-11T09:00:00+08:00</updated><id>https://imageslr.com/2023/ads-infra</id><content type="html" xml:base="https://imageslr.com/2023/ads-infra.html"><![CDATA[]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">💻【C++】研发基本功 - GTest / GMock 单元测试实践手册</title><link href="https://imageslr.com/2023/gtest.html" rel="alternate" type="text/html" title="💻【C++】研发基本功 - GTest / GMock 单元测试实践手册" /><published>2023-06-10T12:00:00+08:00</published><updated>2023-06-10T00:00:00+08:00</updated><id>https://imageslr.com/2023/gtest</id><content type="html" xml:base="https://imageslr.com/2023/gtest.html"><![CDATA[<h2 id="一前言">一、前言</h2>

<p class="ant-alert ant-alert-info">📌 本文来自 Ads Infra 内部分享，<a href="https://imageslr.com/ads-infra.html">欢迎加入 👉🏻</a></p>

<p>作为架构部门，我们的很多核心仓库都是 C++ 编写，目前基本都有 80% 的增量单测覆盖率卡点。编写单测的好处不言而喻：通过构造各种 case，可以发现空指针、大数越界等肉眼不容易发现的 bug。此外，单测也可以在不引流的情况下，测试功能是否正确。因此，编写单测是必要的，<strong>为新增代码补充单测是每个研发同学的基本功</strong>。</p>

<p>但是，<strong>C++ 编写单测也是最麻烦的</strong>。根据日常观察，大部分同学没有系统地写过单测，基本依赖照抄现有代码，单测写得慢，且不标准。此外，没有掌握常见的调试技巧，主要通过 <code>cout</code> 逐行打日志和重新编译来定位问题，进一步降低了单测编写效率。</p>

<p>本文旨在解决上述问题：</p>

<ul>
  <li>本文的<strong>受众</strong>：开发过 C++ 模块、知道 GTest / GMock 的基本使用、编写过单测代码、能完成简单场景的单测需求、但对于复杂的代码则无从下手的同学；写单测感觉很不爽、知道痛点在哪儿、但不知道如何解决的同学；平时 review 代码只看业务逻辑、不看单测合理性的 reviewer 同学。</li>
  <li>本文的<strong>内容</strong>：分享 GTest、GMock 的核心用法、常用技巧 + 单测编写的思路 + GDB 调试方法。只讲最必要的、最常用的内容，能覆盖大部分场景的单测需求。不讲花活儿，但会引用外部文档供扩展阅读。</li>
  <li>本文的<strong>目标</strong>：(1) 通过分享上述内容，让大家系统掌握单测编写和调试方法，<strong>写起来更丝滑</strong>，查问题更高效。(2) 对齐认知，<strong>让单测真正发挥作用</strong>。知道什么是正确的、有效的、好的单测，并写出这样的单测。知道什么是无效的、差的单测，并避免写出 / 合入这样的单测。以对待线上代码的标准来对待单测。</li>
</ul>

<h2 id="二hello-world从一个单测示例开始">二、Hello, world：从一个单测示例开始</h2>

<p>为下面这段代码编写单测：</p>

<pre><code class="language-C++">int check_threshold(RequestContext ctx, Ad ad) {
    if (ad.pricing == CPT) {
        return -1;
    }
    if (ad.pricing == CPM) {
        if (ctx.params.use_stable_thresh || ad.use_stable_thresh()) {
            return 2;
        }
        return ctx.get_threshold(ad);
    }
    ...
}
</code></pre>

<p>编写出来的单测代码可能是这样的：</p>

<pre><code class="language-C++">// Case 1
TEST(ChecksThresholdTest, CheckThreshForCPT) {
    // 1. 构造输入
    RequestContext ctx;
    Ad ad;
    ad.pricing = CPT;
    
    // 2. 检查输出
    EXPECT_EQ(check_threshold(ctx, ad), -1);
}

// Case 2
TEST(CheckThresholdTest, CheckThreshForCPM) {
    // 1. 构造输入
    MockRequestContext ctx;  // 这是一个 GMock 对象
   
    // 使用大括号分隔不同 case
    {
        Ad ad;
        ad.pricing = CPM;
        ctx.params.use_stable_thresh = true;
        EXPECT_EQ(check_threshold(ctx, ad), 2);        
        ctx.params.use_stable_thresh = false;  // reset
    }
        
    // 上面对于 if(a||b) 的分支来说，只达到了 50% 分支覆盖率
    // 尝试达到 100% 覆盖率
    {
        Ad ad;
        ad.pricing = CPM;
        ad.should_use_stable_thresh = true; // 假设 ad.use_stable_thresh() 函数内部用了这个字段来判断
        ASSERT_TRUE(ad.use_stable_thresh()); // 上一行修改是为了控制这个函数的结果，所以最好 ASSERT 一下
        EXPECT_EQ(check_threshold(ctx, ad), 2);  
    }
  
    // 默认分支
    {
        Ad ad;
        ad.pricing = CPM;
        EXPECT_CALL(ctx, get_threshold).WillOnce(Return(100));
        EXPECT_EQ(check_threshold(ctx, ad), 100);
    }
}
</code></pre>

<p>涉及到的方面：</p>

<ul>
  <li>构造输入：手动</li>
  <li>检测输出：EXPECT</li>
  <li>控制外部函数的返回值：EXPECT_CALL</li>
  <li>分支覆盖率：对于 <code>if(a||b)</code>，需要分别构造 <code>a == true</code> 和 <code>b == true</code> 两个 case。</li>
</ul>

<h2 id="三gtest">三、GTest</h2>

<h3 id="31-基本概念test-suitetest-case">3.1 基本概念：Test Suite、Test Case</h3>

<h4 id="test-suite">Test Suite</h4>

<pre><code class="language-C++">TEST(TestSuiteName, TestCaseName) {
    // 单测代码
    EXPECT_EQ(func(0), 0);
}
</code></pre>

<ul>
  <li><code>TestSuiteName</code> 用来汇总 test case，相关的 test case 应该是相同的 <code>TestSuiteName</code>。一个文件里只能有一个 <code>TestSuiteName</code>，建议命名为这个文件测试的类名。</li>
  <li><code>TestCaseName</code> 是测试用例的名称。建议有意义，比如“被测试的函数名称”，或者被测试的函数名的不同输入的情况。</li>
  <li><code>TestSuiteName_TestCaseName</code> 的组合应该是唯一的。</li>
  <li>GTest 生成的类名是带下划线的，所以上面这些名字里不建议有下划线。</li>
</ul>

<h4 id="test-case">Test Case</h4>

<p>一个 <code>TEST(Foo, Bar){...}</code> 就是一个 Test Case。考虑到构造输入有成本，通常一个 <code>TEST(Foo, Bar)</code> 里会反复修改输入，构造多个 case，测试不同的执行流程。这里建议<strong>用大括号分隔不同的 case</strong>，整体更条理。另一个好处在于：<strong>每个变量的生命周期仅限于大括号内</strong>。这样就可以反复使用相同的变量名，而不用给变量名编号。</p>

<pre><code class="language-C++">TEST(Foo, bar) {
    // case 1: enable = true
    {
        Context ctx;
        params.enable_refresh = true;
        ASSERT_EQ(ctx-&gt;is_enable_fresh(), true);
    }
    
    // case 2: enable = false
    {
        Context ctx;
        params.enable_refresh = false;
        ASSERT_EQ(ctx-&gt;is_enable_fresh(), false);
    }
}
</code></pre>

<p>此外，如果待测函数十分复杂，建议拆分多个 <code>TEST(Foo, Bar){...}</code>，避免 Test Case 代码膨胀。比如：</p>

<pre><code class="language-C++">// 待测函数
int foo(Ad ad) {
    if (!ad)
        return -1;
    switch(ad.pricing) {
        case CPT:
            ...
        case GD:
            ...
    }
}
// 输入为空
TEST(Foo, IsNil) {
    ...
}

// 输入是 CPT 广告
TEST(Foo, IsCpt) {
    ...
}

// 输入是 GD 广告
TEST(Foo, IsGd) {
    ...
}
</code></pre>

<h3 id="32-善用-test_f避免写重复的代码">3.2 善用 TEST_F，避免写重复的代码</h3>

<p>GTest 提供了多种测试宏，其中最为常用的是 <code>TEST</code>、<code>TEST_F</code>，它们的区别如下：</p>

<ol>
  <li><code>TEST</code>：这是最基本的测试宏，代表一个最小测试单元。在执行 <code>TEST</code> 宏时，gtest 会为每个 <code>TEST</code> 定义一个独立的实例，使其互相隔离，避免对同一个变量进行修改或共享等可能带来的副作用。</li>
  <li><strong><code>TEST_F</code></strong>：这是 TestFixture 的测试宏。TestFixture 是一个类，可以在<strong>多个</strong><strong>测试用例</strong><strong>之间共享数据结构或方法</strong>。对于同一个 Test Suite 的所有 Test Cases，会创建一个 TestFixture 对象，其 SetUp 函数会在每个 Test Case 执行之前被调用，而 TearDown 函数则会在每个 Test Case 执行之后被调用。</li>
</ol>

<p>使用 Test Fixture Class，可以避免写重复的代码：</p>

<ul>
  <li>将共享的变量作为成员变量，可以在 test case 中直接访问；变量初始化、回收逻辑放到 SetUp()、TearDown()</li>
  <li>提供公共方法，可以在 test case 中直接使用</li>
</ul>

<p>示例代码：</p>

<pre><code class="language-C++">class FooTest : public ::testing::Test {
protected:
  // 在每个 Test Case 运行开始前，都会调用 SetUp，这里可以初始化
  void SetUp() override {
    ctx = RequestContext("123");
  }
  
  // 在每个 Test Case 运行结束后，都会调用 TearDown
  void TearDown() override {}
  
  // 所有 Test Case 都可以直接访问这些变量和方法
  Ad new_ad() { return Ad(ctx); }
  RequestContext ctx;
};

TEST_F(FooTest, enable_foo) { // 这里会初始化 FooTest 对象
  ctx-&gt;params.enable_foo = true; // 可以访问 FooTest 中的变量
  auto item = new_ad(); // 可以调用 FooTest 中的方法
  ...
}

// 每个 test case 都是独立的，这里会初始化另一个 FooTest 对象
TEST_F(FooTest, OnTestProgramStart) { 
  // ...
}
</code></pre>

<p>实际使用技巧：</p>
<ul>
  <li>共享一些变量，比如预先初始化好单测依赖的 Context 对象</li>
  <li>封装一些公共方法，尤其是构造通用数据对象的方法</li>
  <li>派生更多子类：
    <ul>
      <li>建议每个服务有一个公共的 <code>BaseTestFixture</code>，继承 <code>::testing::Test</code>，封装全局通用的方法</li>
      <li>其他单测可以再继承 <code>BaseTestFixture</code>，提供某个测试场景下共享变量和方法</li>
    </ul>
  </li>
</ul>

<h3 id="33-断言expect-与-assert-宏">3.3 断言：EXPECT 与 ASSERT 宏</h3>

<p>用来判断某个变量的值是否符合预期。前者在校验失败时会打印失败信息，然后继续运行。后者会直接终止。</p>

<div class="ant-alert ant-alert-info">

  <p class="mb-1">💡 <strong>正确使用 ASSERT 和 EXPECT 前缀：</strong></p>

  <ul>
    <li>如果某个判断不通过时，会影响后续步骤，要使用 ASSERT。常见的是空指针，或者数组访问越界。
      <blockquote class="mb-1">
        <p>如果某个 EXPECT 失败会导致后续一连串 EXPECT 失败，那么第一个 EXPECT 应该换成 ASSERT。这就像编译时的报错信息，往往只有第一个是有用的，其他错误都只是刷屏。</p>
      </blockquote>
    </li>
    <li>其他情况，可以使用 EXPECT，尽可能多测试几个用例。</li>
  </ul>

</div>

<p>下面罗列一些最常用的 EXPECT 宏，把前缀换成 ASSERT 也可以使用。完整列表见<a href="http://google.github.io/googletest/reference/assertions.html">文档</a>。</p>

<h4 id="1-一元--二元比较">(1) 一元 / 二元比较</h4>

<ul>
  <li><code>EXPECT_TRUE(foo)</code>、<code>EXPECT_FALSE(foo)</code>：判断一个变量是否是 true 或 false。</li>
  <li>二元比较：
    <ul>
      <li><code>EXPECT_EQ(foo, bar)</code>：判断两个变量是否相等。
        <ul>
          <li>只要重载了<code>==</code>运算符就可以，所以也可以判断两个 vector 是否相等。</li>
        </ul>
      </li>
      <li><code>EXPECT_NE(foo, bar)</code>：判断 foo != bar。</li>
      <li><code>EXPECT_LT(foo, bar)</code>：foo &lt; bar，less than。</li>
      <li><code>EXPECT_LE(foo, bar)</code>: foo ≤ bar，less or equal。</li>
      <li><code>EXPECT_GT(foo, bar)</code>：foo &gt; bar，greater than。</li>
      <li><code>EXPECT_GE(foo, bar)</code>: foo ≥ bar，greater or equal。</li>
    </ul>
  </li>
</ul>

<h4 id="2-浮点数比较">(2) 浮点数比较</h4>

<ul>
  <li><code>EXPECT_DOUBLE_EQ(foo, 0.1)</code>：浮点数比较不能使用 <code>EXPECT_EQ</code>。</li>
  <li><code>EXPECT_FLOAT_EQ</code>：同上。</li>
  <li>
    <p><code>EXPECT_NEAR(foo, bar, abs_val)</code>：判断两个数字的绝对值相差是否小于等于 abs_val。</p>

    <pre><code class="language-C++">double pi = 3.141592653589793238;
double approx_pi = 3.14;
EXPECT_NEAR(pi, approx_pi, 0.01);  // 检测两个 π 值，允许误差在 0.01 以内
</code></pre>
  </li>
</ul>

<h4 id="3-字符串比较">(3) 字符串比较</h4>

<ul>
  <li>
    <p><code>EXPECT_STREQ(foo, "bar")</code>：判断两个字符串是否相等。这里比较的是 C 风格的字符串，即 <code>char*</code>。如果某个对象是 <code>std::string</code>，需要调用其 <code>c_str()</code> 方法。如果两个对象都是 <code>std::string</code>，可以使用 <code>EXPECT_EQ</code>。</p>

    <pre><code class="language-C++">std::string str = "hello";
EXPECT_STREQ(str.c_str(), "hello");
</code></pre>
  </li>
  <li><code>EXPECT_STRNE</code>：不相等。</li>
  <li><code>EXPECT_STRCASEEQ</code>：忽略大小写，是否相等。</li>
  <li><code>EXPECT_STRCASENE</code>：忽略大小写，是否相等。</li>
</ul>

<h4 id="4-其他">(4) 其他</h4>

<ul>
  <li>
    <p><code>EXPECT_THROW</code>/<code>EXPECT_NO_THROW</code>：处理异常，不要自行<code>try-catch</code>。</p>
  </li>
  <li>
    <p><code>EXPECT_THAT</code>：这实际上是 GMock 提供的宏，需要和 <a href="#matcher">匹配器 Matcher</a> 配合使用，详见下文。这是<strong>写出优雅单测的必备技能</strong>。</p>
  </li>
  <li>
    <p><code>EXPECT_CALL</code>：同样是 GMock 提供的宏，判断函数被调用的次数，详见下文。</p>
  </li>
  <li>
    <p><code>EXPECT_PRED(func, arg1, arg2, ...)</code>：自定义一个返回 bool 的谓词，传给该谓词一系列参数，判断是否返回 true。如果失败，会依次打印传入的参数值。</p>

    <pre><code class="language-C++">std::vector&lt;int&gt; vec = {1, 2, 3};
EXPECT_PRED([](const std::vector&lt;int&gt;&amp; v) { return v.size() == 3; }, vec);
</code></pre>
  </li>
</ul>

<h4 id="断言失败时输出自定义信息">断言失败时输出自定义信息</h4>

<p>默认当 EXPECT 或 ASSERT 失败时，GTest 会打印预期值和实际值：</p>

<pre><code class="language-C++">EXPECT_EQ(4, 3);

/path/to/test.cpp:7: Failure
Expected equality of these values:
  4
  result
    Which is: 3
</code></pre>

<p>但有时候，这些信息不够定位具体的失败原因。可以像这样<strong>输出自定义日志</strong>，这些日志仅在 EXPECT 失败时才打印：</p>

<pre><code class="language-C++">for (int i = 0; i &lt; x.size(); i++) {
  EXPECT_EQ(x[i], y[i]) &lt;&lt; "x and y differ at index " &lt;&lt; i;
}
</code></pre>

<p>还可以<strong>在 TestFixture 中封装 debug 函数</strong>，输出更详细的信息。比如，被测对象中包含了一些位图 std::bitset。在 EXPECT 失败时打印位图信息，有助于排查单测失败的原因：</p>

<pre><code class="language-C++">class BitsetTest : public BaseTest {
public:
  std::string debug_message() {
      stringstream ss;
      for (const auto&amp; iter : bitset_maps) {
        ss &lt;&lt; "bitset: name=" &lt;&lt; iter.first &lt;&lt; " value=" &lt;&lt; iter.second &lt;&lt; std::endl;
      }
      return ss.string();
  }
}

TEST_F(BitsetTest, validate) {
    // ...
    EXPECT_TRUE(validate(ad, pos)) &lt;&lt; debug_message();
}
</code></pre>

<h2 id="四gmock">四、GMock</h2>

<h3 id="41-原理与示例">4.1 原理与示例</h3>

<p>GMock 是 Google Test 提供的一个 C++ mocking 框架，可以用于创建虚拟的对象和方法。GMock 的原理是利用 C++ 的多态特性，覆盖 virtual 函数，将函数调用转发到相应的 mock 函数中。</p>

<p>GMock 基本使用流程如下：</p>

<ol>
  <li>继承被 mock 的类，定义一个新的 Mock 类</li>
  <li>使用 GMock 提供的 mock 宏，用于实现 Mock 类的方法</li>
  <li>通过上面的 Mock 类，创建一个模拟对象</li>
  <li>通过 EXPECT_CALL 宏，控制模拟方法的返回值</li>
</ol>

<pre><code class="language-C++">#include &lt;gmock/gmock.h&gt;

class FooInterface {
public:
    virtual int foo(int) { return 3; } // ① 需要定义为虚函数
};

// ② 需要声明一个 Mock 类，并声明 MOCK_METHOD
class MockFoo: public FooInterface {
public:
    MOCK_METHOD1(foo, int(int)); // 记录函数名字 + 类型信息到 MockFoo 对象上
};

using ::testing::Return;
TEST(FooInterface, foo) {
    MockFoo mockFoo; // ③ 需要声明 Mock 出来的子类
    EXPECT_CALL(mockFoo, foo(3)).Times(1). // 自定义函数返回值
                WillOnce(Return(10));
    EXPECT_EQ(mockFoo.foo(3), 10); // return 10
}
</code></pre>

<p>使用 GMock 有<strong>两个前提</strong>：(1) 被 Mock 的方法必须是虚函数；(2) 必须替换掉被 mock 的对象，将其赋值为 mock 对象。其<strong>不足之处</strong>：(1) 使用 GMock 时必须定义一个 Mock class；(2) 如果想 mock 非虚函数，需要变更函数签名，这可能不太安全；(3) 对于函数内部的局部变量，无法赋值，也就无法 mock。</p>

<h3 id="42-expect_call">4.2 EXPECT_CALL</h3>

<p>语法：</p>

<pre><code class="language-C++">EXPECT_CALL(mock_object, method(matchers))
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);
</code></pre>

<p>比如下面代码的含义是：调用 <code>turtle</code> 对象的 <code>GetX(string)</code> 方法 5 次，每次传入的参数都是”hello”，第一次返回 100，第二次返回 150，之后几次返回 200：</p>

<pre><code class="language-C++">using ::testing::Return;
...
EXPECT_CALL(turtle, GetX("hello"))
    .Times(5)
    .WillOnce(Return(100))
    .WillOnce(Return(150))
    .WillRepeatedly(Return(200));
</code></pre>

<h4 id="基数判断函数调用次数">基数：判断函数调用次数</h4>

<ul>
  <li><code>Times(n)</code>：调用 n 次</li>
  <li><code>Times(0)</code>：不被调用</li>
  <li><code>Times(AtLeast(n))</code>：至少被调用 n 次</li>
  <li><code>WillOnce(action)</code>：被调用 1 次，执行自定义行为</li>
  <li><code>WillRepeatedly(action)</code>：被调用任意次，执行自定义行为</li>
</ul>

<h4 id="action控制被调用时的行为">Action：控制被调用时的行为</h4>

<p><code>Will</code> 开头的接口可以传入一个 Action 参数，设置 mock 函数被调用时的行为。常用的：</p>

<ul>
  <li>
    <p><code>Return</code>：返回指定值。比如 <code>WillOnce(Return(100))</code>。</p>
  </li>
  <li>
    <p><code>ReturnRef</code>、<code>ByRef</code>：<code>Return</code> 不支持返回引用类型的变量，需要用这两个宏。</p>
  </li>
  <li>
    <p><code>SetArgReferee&lt;n&gt;(value)</code>：修改传入的第 n 个引用类型的参数的值，下标 n 从 0 开始。</p>

    <pre><code class="language-C++">class MockGetter : public Getter {
    public:
    MOCK_METHOD(int, get, (const string&amp;, string&amp;));
};
  
TEST(MockGetter, SetArgRefereeTest) {
    const std::string key = "foo_key";
    std::string value;
  
    MockGetter getter;
    EXPECT_CALL(getter, get(key, _))
        .WillOnce(SetArgReferee&lt;1&gt;("bar_value"));
  
    getter.get(key ,value);
    EXPECT_EQ(sum, "bar_value");
}
</code></pre>
  </li>
  <li>
    <p><code>DoAll(action1, action2, ...)</code>：执行多个 Action，比如修改参数的值 + 设定返回值：</p>

    <pre><code class="language-C++">EXPECT_CALL(calc, Add(_, _, _))
  .WillOnce(DoAll(SetArgReferee&lt;2&gt;(8), Return(true)));
</code></pre>
  </li>
  <li>
    <p>直接传入一个 lambda 函数，或者 <code>Invoke(function)</code>：执行自定义的函数，比如：</p>

    <pre><code class="language-C++">// 传入 lambda 函数
EXPECT_CALL(calc, Add).WillOnce([](int a, int b)) {
    return a + b + 1;
});
  
// 传入函数指针
int AddFunc(int a, int b) {
    return a + b + 1;
}
EXPECT_CALL(calc, Add(_, _)).WillOnce(Invoke(AddFunc));
  
// 传入类方法
class AddHelper {
    public:
    int Add(int a, int b) {
        return a + b + 1;
    }
};
AddHelper helper;
EXPECT_CALL(calc, Add(_, _)).WillOnce(Invoke(&amp;helper, &amp;AddHelper::Add));
</code></pre>

    <ul>
      <li>Lambda 函数的签名必须和被 Mock 的函数一致。</li>
      <li>Invoke 函数可接受任何可调用对象作为参数，包括函数指针、函数对象、Lambda 表达式等</li>
    </ul>
  </li>
</ul>

<h4 id="matcher">Matcher：匹配传给函数的参数</h4>

<p>Matcher 能够实现在复杂场景下进行断言，可以让测试用例更加灵活和可读，是写出优雅单测的必备工具。</p>

<p>Matcher 提供了一系列常用的比较函数，例如 Eq、Ne、Lt、Gt、Le、Ge 等，可以满足不同类型变量的比较。</p>

<p>Matcher 有两个使用场景：</p>

<ol>
  <li>
    <p>和 EXPECT_CALL 配合使用，用于检查传递给函数的参数值是否符合预期</p>

    <pre><code class="language-C++">// 期望第一个参数大于 2，第二个参数小于 6
EXPECT_CALL(calc, Add(Gt(2), Le(6)));
calc.Add(3, 5);  // 可以通过检测
calc.Add(2, 7);  // 不能通过检测
</code></pre>
  </li>
  <li>
    <p>和 EXPECT_THAT 配合使用，用于检查某个变量的值是否符合预期</p>

    <pre><code class="language-C++">// int_foo &gt; 6
EXPECT_THAT(int_foo, Gt(6));
   
// 判断一个 vector 的元素值
std::vector&lt;int&gt; result = {1, 2, 5};
EXPECT_THAT(result, ElementsAre(1, 2, Gt(3)));
   
// 判断一个 unordered_map 的元素值
std::unordered_map&lt;string, int&gt; result = {{"idt_a", 1}, {"idt_b"， 2}};
EXPECT_THAT(result, UnorderedElementsAre(Pair("idt_a", 1), Pair("idt_b", 2)));
   
// 期望 foo 包含子串 "hello"
EXPECT_THAT(foo, HasSubStr("hello"));  
</code></pre>
  </li>
</ol>

<h5 id="通配符_atype">通配符：<code>_</code>，<code>A&lt;type&gt;</code></h5>

<p><code>_</code> 可以匹配任意类型的任意变量。它位于 <code>::testing</code> 命名空间下。示例：</p>

<pre><code class="language-C++">using namespace testing;
EXPECT_CALL(calc, Add(_, _)).Times(1);
EXPECT_CALL(calc, Add).Times(1);  // 省略参数列表，和上面等价
</code></pre>

<p><code>A&lt;type&gt;()</code> 或者 <code>An&lt;type&gt;()</code> 匹配类型是 <code>type</code> 的任意变量。其应用场景主要是匹配重载函数。示例：</p>

<pre><code class="language-C++">class Foo {
    void DoSomething(int a, int b);
    void DoSomething(int a, string b);
}

EXPECT_CALL(foo, DoSomething(_, A&lt;int&gt;()));  // 预期调用第一个函数
</code></pre>

<h5 id="常用匹配器">常用匹配器</h5>

<p>完整列表见 <a href="http://google.github.io/googletest/reference/matchers.html">http://google.github.io/googletest/reference/matchers.html</a>，下面罗列常用的匹配器：</p>

<ul>
  <li>一般比较
    <ul>
      <li>
        <p><code>value</code>：写出字面量的值，就是精确匹配，等价于 <code>Eq(value)</code>。</p>

        <pre><code class="language-C++">EXPECT_CALL(foo, method(100)).Times(1); 
EXPECT_CALL(foo, method(Eq(100))).Times(1);  // 和上面等价
</code></pre>
      </li>
      <li>
        <p><code>Ge(value)</code>、<code>Gt</code>、<code>Le</code>、<code>Lt</code>：&gt;= (greater or equal)、&gt; (greater)、&lt;= (less or equal)、&lt; (less)。</p>

        <pre><code class="language-C++">EXPECT_THAT(int_foo, Gt(100));  // int_foo &gt; 100
EXPECT_THAT(int_foo, Le(200));  // int_foo &lt;= 200
</code></pre>
      </li>
      <li><code>Ne(value)</code>：不等于，not equal。</li>
      <li><code>IsFalse()</code>、<code>IsTrue()</code>：转成 bool 值后是 false 或 true。非 0 值、非空指针等都可以视为 true。</li>
      <li><code>IsNull()</code>、<code>NotNull()</code>：指针是否为空。</li>
    </ul>
  </li>
  <li>浮点数比较
    <ul>
      <li>
        <p><code>DoubleEq(a_double)</code>、<code>FloatEq(a_float)</code>：浮点数相等。</p>

        <pre><code class="language-C++">double foo = 0.01 + 0.02;
EXPECT_THAT(foo, Eq(0.03));  // 会失败
EXPECT_THAT(foo, DoubleEq(0.03));  // 会成功
</code></pre>
      </li>
      <li>
        <p><code>DoubleNear(a_double, max_abs_error)</code>：浮点数近似，差值的绝对值小于给定的 <code>abs_error</code>。</p>

        <pre><code class="language-C++">double foo = 0.03 + 0.001;
EXPECT_THAT(foo, DoubleNear(0.01));   // 会失败
EXPECT_THAT(foo, DoubleNear(0.001));  // 会成功
</code></pre>
      </li>
      <li>
        <p><code>FloatNear(a_float, max_abs_error)</code>：同上。</p>
      </li>
    </ul>
  </li>
  <li>字符串比较
    <ul>
      <li><code>StartsWith(prefix)</code>：指定前缀</li>
      <li><code>EndsWith(suffix)</code>：指定后缀</li>
      <li>
        <p><code>HasSubstr(string)</code>：包含子串</p>

        <pre><code class="language-C++">std::string str = "hello, world";
EXPECT_THAT(str, StartsWith("hello"));
EXPECT_THAT(str, EndsWith("world"));
EXPECT_THAT(str, HasSubstr("llo"));
</code></pre>
      </li>
      <li><code>IsEmpty()</code>：字符串为空</li>
      <li>
        <p><code>StrEq(string)</code>、<code>StrNe(string)</code>：字符串相等或不等</p>

        <pre><code class="language-C++">EXPECT_CALL(m, foo(StrEq("hello, world")).Times(1);
m.foo("hello, world");  // 符合预期
</code></pre>
      </li>
      <li>
        <p><code>StrCaseEq(string)</code>、<code>StrCaseNe(string)</code>：忽略大小写，字符串相等或不等</p>

        <pre><code class="language-C++">EXPECT_CALL(m, foo(StrCaseEq("hello, world")).Times(1);
m.foo("HELLO, WORLD");  // 符合预期
</code></pre>
      </li>
      <li><code>ContainsRegex(string)</code>：正则表达式匹配</li>
    </ul>
  </li>
  <li>容器比较
    <ul>
      <li>
        <p><code>ElementsAre(e0, e1, ..., en)</code>：每个元素依次是什么，用于 vector、map、set 等。</p>

        <pre><code class="language-C++">std:vector&lt;int&gt; v = {1, 2, 4};
EXPECT_THAT(v, ElementsAre(1, 2, 4));  // 值是 {1, 2, 4}
EXPECT_THAT(v, ElementsAre(1, 2, Gt(3)));  // 值是 {1, 2, 大于 3 的任意值}
    
// 下面这样也可以，但不如上面只写一行优雅，不推荐
vector&lt;int&gt; expect_vector = {1, 2, 4};
EXPECT_EQ(v, expected_vector);
</code></pre>
      </li>
      <li>
        <p><code>UnorderedElementsAre(e0, e1, ..., en)</code>：同上，用于 unordered_set、unordered_map 等。</p>

        <pre><code class="language-C++">std:set&lt;int&gt; v = {1, 2, 4};
EXPECT_THAT(v, UnorderedElementsAre(1, 2, 4));  // 值包含 {1, 2, 4}
EXPECT_THAT(v, UnorderedElementsAre(1, 2, Gt(3)));  // 值包含 {1, 2, 大于 3 的任意值}
</code></pre>
      </li>
      <li><code>ContainerEq(container)</code>：效果同上，但会打印出哪些元素不一致。</li>
      <li><code>Contains(e)</code>：包含一个元素和 <code>e</code> 匹配，这里 <code>e</code> 可以是一个精确值，也可以是一个匹配器。</li>
      <li>
        <p><code>Contains(e).Times(n)</code>：检测 <code>e</code> 指定的元素出现 n 次。<code>Times(0)</code> 表示不能包含这样的元素。</p>

        <pre><code class="language-C++">EXPECT_THAT(v, Contains(5));  // 需要包含一个值为 5 的元素
EXPECT_THAT(v, Contains(Lt(5)));  // 需要包含一个值 &lt;5 的元素
EXPECT_THAT(v, Contains(Lt(5)).Times(3));  // 需要包含 3 个值 &lt;5 的元素
</code></pre>
      </li>
      <li>
        <p><code>Each(e)</code>：每个元素都要匹配 <code>e</code>。</p>

        <pre><code class="language-C++">EXPECT_THAT(v, Each(Lt(5)));  // 每个元素都需要 &lt;5
EXPECT_THAT(v, Each(AllOf(Lt(5), Gt(3)));  // 每个元素都需要 &gt;3 且 &lt;5
</code></pre>
      </li>
      <li><code>IsSubsetOf(array)</code>、<code>IsSubsetOf(begin, end)</code>：参数是指定数组的子集，顺序可以不一致。</li>
    </ul>
  </li>
  <li>成员匹配
    <ul>
      <li>
        <p><code>Field(&amp;class::field, m)</code>：匹配字段值，用于结构体检测，比如：</p>

        <pre><code class="language-C++">struct MyStruct {
    int value = 42;
    std::string greeting = "aloha";
};
MyStruct s;
EXPECT_THAT(s, FieldsAre(42, "aloha"));
</code></pre>
      </li>
      <li>
        <p><code>Pair(m1, m2)</code>：匹配一个 <code>std::pair</code>，经常和 <code>ElementsAre</code> 配合使用，匹配一个 map：</p>

        <pre><code class="language-C++">std::map m = {
    {"hello", 1},
    {"world", 2},
};
EXPECT_THAT(m, ElementsAre(Pair("hello", 1), Pair("world", 2)));
</code></pre>
      </li>
    </ul>
  </li>
  <li>指针匹配
    <ul>
      <li>
        <p><code>Pointee</code>：匹配一个指针或 shared_ptr，常和 <code>Field</code> 一起检测某个指针的字段值：</p>

        <pre><code class="language-C++">struct Item { int id; };
std::shared_ptr&lt;Item&gt; obj = std::make_shared&lt;Item&gt;();
obj-&gt;id = 1;
EXPECT_THAT(obj, Pointee(Field(&amp;Item::id, 1));
</code></pre>
      </li>
    </ul>
  </li>
  <li>复合匹配
    <ul>
      <li>
        <p><code>AllOf(m1, m2, ..., mn)</code>：匹配所有给定的匹配器。</p>

        <pre><code class="language-C++">std:vector&lt;int&gt; v = {4, 5};
EXPECT_THAT(v, Each(AllOf(Le(5), Gt(3)));  // 每个元素都需要 &gt;3 且 &lt;=5
</code></pre>
      </li>
      <li>
        <p><code>AnyOf(m1, m2, ..., mn)</code>：匹配任何一个给定的匹配器。</p>

        <pre><code class="language-C++">std:vector&lt;int&gt; v = {1, 2, 6, 7};
EXPECT_THAT(v, Each(AnyOf(Lt(3), Gt(5)));  // 每个元素要么 &lt;3，要么 &gt;5
</code></pre>
      </li>
      <li>
        <p><code>Not(m)</code>：不匹配给定的匹配器，可以和 <code>AllOf</code>、<code>AnyOf</code> 配合使用。</p>

        <pre><code class="language-C++">EXPECT_THAT(v, Each(AllOf(Gt(3), Lt(5)));  // 3 &lt; each_item &lt; 5
EXPECT_THAT(v, Each(Not(AllOf(Gt(3), Lt(5))));  // each_item &lt;= 3 || each_item &gt;=5
</code></pre>
      </li>
      <li>
        <p><code>Conditional(cond, m1, m2)</code>：cond 为 true 时匹配 <code>m1</code>，否则匹配 <code>m2</code>。</p>

        <pre><code class="language-C++">EXPECT_THAT(v, Conditional(is_ad, Gt(5), Lt(3));  // v = is_ad ? v &gt; 5 : v &lt; 3
</code></pre>
      </li>
    </ul>
  </li>
</ul>

<h5 id="匹配器的优先级">匹配器的优先级</h5>

<p>在使用 GMock 的 EXPECT_CALL 宏进行 mock 函数参数匹配时，一次函数调用可能命中多个匹配器：</p>

<pre><code class="language-C++">EXPECT_CALL(calc, add).Times(1);                // 任意参数
EXPECT_CALL(calc, add(_, _)).Times(1);          // 和上面等价
EXPECT_CALL(calc, add(3, 5)).Times(1);          // 字面量，精确匹配
EXPECT_CALL(calc, add(Gt(2), Lt(6))).Times(1);  // 比较，模糊匹配

calc.add(3, 5);  // 这一行理论上可以匹配上面每一个 EXPECT_CALL
</code></pre>

<p>匹配的优先级如下：<strong>模糊匹配器 &gt; 精确匹配器 &gt;</strong> <strong>通配符</strong></p>

<ul>
  <li>模糊匹配器：Lt (小于)、Gt (大于) 等</li>
  <li>精确匹配器：字面量、Eq (相等) 等</li>
  <li>通配符：_ 等</li>
  <li>当优先级相同时，越近的声明优先级越高。</li>
</ul>

<p>这引入了一些使用技巧：</p>

<ol>
  <li>
    <p>只设置必要的匹配器。如果对某个参数的值不感兴趣，请写 <code>_</code> 作为参数，这意味着“一切皆有可能”。</p>

    <pre><code class="language-C++">EXPECT_CALL(calc, add(5, _).Times(1);  // 如果只关心第一个参数的值，第二个参数就写成 _
EXPECT_CALL(calc, add(5, 3).Times(1);  // 如果这样写，之后代码变动，单测可能就不通过了
</code></pre>
  </li>
  <li>
    <p>如果对所有参数的值都不感兴趣，可以<strong>省略参数列表</strong>，这和把每个参数都写成 <code>_</code> 是一致的。好处是后续改了函数签名后，比如新增了一个参数，单测是不需要改动的。</p>

    <pre><code class="language-C++">EXPECT_CALL(calc, add).Times(1);       // 任意参数
EXPECT_CALL(calc, add(_, _)).Times(1); // 和上面等价
</code></pre>
  </li>
  <li>
    <p>利用匹配器的优先级，可以细粒度地<strong>控制函数在不同参数下的返回值</strong>。比如 mock 一个 getter，我们希望在 key == <code>foo</code> 时返回 <code>bar</code>、key == <code>hello</code> 时返回 <code>world</code>，其他 key 通通返回空字符串，那么可以这样写：</p>

    <pre><code class="language-C++">EXPECT_CALL(getter, get).WillRepeatedly(Return(""));
EXPECT_CALL(getter, get("foo")).WillRepeatedly(Return("bar"));
EXPECT_CALL(getter, get("hello")).WillRepeatedly(Return("world"));
   
EXPECT_STREQ(getter.get("foo"), "bar");
EXPECT_STREQ(getter.get("hello"), "world");
EXPECT_STREQ(getter.get("aaa"), "");
EXPECT_STREQ(getter.get("bbb"), "");
</code></pre>
  </li>
</ol>

<h4 id="uninteresting-call处理非预期调用">Uninteresting call：处理非预期调用</h4>

<p><code>非预期调用</code>是指未被 <code>EXPECT_CALL</code> 匹配的调用。当有非预期调用时，会有 warning 日志输出：</p>

<pre><code class="language-plaintext">Uninteresting mock function call - returning default value.
    Function call: foo(42)
          Returns: 0
</code></pre>

<p>有两种处理方式。</p>

<h5 id="nicemock不要输出-warning-信息">NiceMock：不要输出 warning 信息</h5>

<p>GMock 有三种级别：Nice Mock、Naggy Mock、Strict Mock。</p>

<p>默认是 Naggy Mock，当有非预期调用时，输出 warning 日志。</p>

<pre><code class="language-plaintext">Uninteresting mock function call - returning default value.
    Function call: foo(42)
          Returns: 0
</code></pre>

<p>如果我们希望非预期调用不要有 warning，可以用 <code>NiceMock</code>。<code>NiceMock</code> 是一个模板类：</p>

<pre><code class="language-C++">class MyMockClass : public MyClass {
    MOCK_METHOD(...)
};
MyMockClass mock;  // 这非预期调用会有 warning 日志
NiceMock&lt;MyMockClass&gt; mock; // 改成这样就不会有 warning 日志了
</code></pre>

<p>也可以在 Mock Class 定义的时候，直接继承 <code>NiceMock</code>：</p>

<pre><code class="language-C++">class MyMockClass : public NiceMock&lt;MyClass&gt; {
    MOCK_METHOD(...)
};
MyMockClass mock;  // 这里非预期调用会返回默认值，不会有 warning 日志
</code></pre>

<p>Strict Mock 在有非预期调用时会直接 fail。也是一个模板类，使用方法和 <code>NiceMock</code> 类似。</p>

<h5 id="打印调用栈检查非预期调用来自哪里">打印调用栈：检查非预期调用来自哪里</h5>

<p>当有非预期调用时，如果我们希望检查非预期调用来自哪里，可以打印调用栈。有两种方式。</p>

<ul>
  <li>
    <p>一种是通过 EXPECT_CALL 打印调用栈：</p>

    <pre><code class="language-C++">#include &lt;boost/stacktrace.hpp&gt;
  
void print_stack_trace() {
  std::cout &lt;&lt; "call stack:" &lt;&lt; std::endl;
  const auto frames = boost::stacktrace::stacktrace();
  for (const auto&amp; frame : frames) {
    std::cout &lt;&lt; "  " &lt;&lt; frame &lt;&lt; std::endl;
  }
}
  
EXPECT_CALL(...).WillRepeatedly([](){
  print_stack_trace();
  return xxx;  // 返回默认值
});
</code></pre>
  </li>
  <li>
    <p>另一种方式是使用 GTest 提供的选项 <code>--gmock_verbose=info</code>，该选项会打印每次 Mock Method 被调用时的参数和调用栈。需要在单测 main 函数执行 <code>::testing::InitGoogleMock(&amp;</code><em><code>argc</code></em><code>, </code><em><code>argv)</code>**。</em></p>
  </li>
</ul>

<h3 id="43-on_call">4.3 ON_CALL</h3>

<p>ON_CALL 可以和 EXPECT_CALL 配合使用。ON_CALL 设置函数的默认行为，EXPECT_CALL 临时修改其行为。</p>

<p class="ant-alert ant-alert-warning">💡 ON_CALL 和 EXPECT_CALL 的语法很像，但提供了不同的语义。EXPECT_CALL 目的在于定义一个预期，即我们期望被测试函数在某些特定条件下应该调用哪些函数，如果没有满足预期的调用，则认为是一次失败。ON_CALL 只是为了指定被测试函数的默认行为。</p>

<p>ON_CALL 通常用在 <strong>Mock 类的构造函数</strong>、或者 TestFixture 的 <strong><code>SetUp</code></strong> <strong>函数</strong>里：</p>

<ol>
  <li>令 mock 函数始终返回某个自定义的值</li>
  <li>
    <p>将 mock 函数的默认操作委托给基类或其他实例进行。一个具体使用场景：希望 Mock 某个函数，默认还是执行原有操作，但当有需要的时候，可以临时更改其行为。这时就可以在 ON_CALL 里把默认操作委托给基类，后续再在 EXPECT_CALL 里临时控制其返回值。</p>

    <pre><code class="language-C++"> class MockFoo : public Foo {
 public:
 // Normal mock method definitions using gMock.
 MOCK_METHOD(char, DoThis, (int n), (override));
 MOCK_METHOD(void, DoThat, (const char* s, int* p), (override));
      
 // 构造函数里，委托 Mock 接口的操作给其他类
 MockFoo() {
 // 委托给基类
 ON_CALL(*this, DoThat).WillByDefault([this](const char* s, int* p) {
 Foo::DoThat(s, p);
 });
 // 委托给另一个对象
 ON_CALL(*this, DoThis).WillByDefault([this](int n) {
 return fake_.DoThis(n);
 });
 }
      
 private:
 FakeFoo fake_;  // Keeps an instance of the fake in the mock.
 };
</code></pre>
  </li>
</ol>

<h2 id="五tips">五、Tips</h2>

<h3 id="51-编译参数">5.1 编译参数</h3>

<h4 id="访问私有变量">访问私有变量</h4>

<p>错误的做法：<code>#define private public</code>，或者定义 getter 函数。前者可能导致编译报错，后者需要修改代码。</p>

<p>正确的做法：<code>-fno-access-control</code>，放在单测的 optimize 参数里。</p>

<h4 id="修改-const-字段">修改 Const 字段</h4>

<p>错误的做法：定义 setter 函数。需要修改代码。</p>

<p>较好的做法：使用 <code>const_cast&lt;Type&amp;&gt;</code> 修改常量类型。</p>

<h4 id="优化级别改为-o0">优化级别改为 O0</h4>

<p>好处：单测覆盖率报告更准。</p>

<h3 id="52-运行单测">5.2 运行单测</h3>

<h4 id="运行特定单测--gtest_filter">运行特定单测：<code>--gtest_filter</code></h4>

<p>什么时候需要运行特定单测：</p>

<ul>
  <li>运行所有单测，发现某个单测失败了。但这个时候单测日志已经刷屏，看不到这个单测的具体失败原因了。</li>
  <li>修复单测 bug 后重新编译，只希望运行上次失败的那个单测。</li>
</ul>

<p>语法：<code>--gtest_filter=TestSuite.TestCase</code>。<strong>支持</strong><strong>通配符</strong> <strong><code>\*</code></strong> <strong>和排除符</strong> <strong><code>-</code></strong>。</p>

<ul>
  <li><code>--gtest_filter=FooTest.Bar</code>，只运行 <code>FooTest.Bar</code>。</li>
  <li><code>--gtest_filter=*FooTest*</code>，运行所有名称里包含 <code>FooTest</code> 的单测。</li>
  <li><code>--gtest_filter=FooTest.*:BarTest.*</code>，运行 FooTest 和 BarTest 两个 suites 下的所有单测。</li>
  <li><code>--gtest_filter=FooTest.*-FooTest.Bar</code>，运行 FooTest 下的所有单测，但不运行 FooTest.Bar。</li>
  <li><code>--gtest_filter=FooTest.*:BarTest.*-FooTest.Bar:BarTest.Foo</code>，运行 FooTest 和 BarTest 下的所有单测，但不运行 FooTest.Bar 和 BarTest.Foo。</li>
  <li>详细的匹配规则见<a href="https://github.com/google/googletest/blob/main/docs/advanced.md#running-a-subset-of-the-tests">文档</a>。</li>
</ul>

<h4 id="重复运行单测多次--gtest_repeat--gtest_break_on_failure">重复运行单测多次：<code>--gtest_repeat</code>、<code>--gtest_break_on_failure</code></h4>

<p>有些单元测试涉及到多线程，可能会偶发性的不通过。</p>

<p>可以使用 <code>--gtest_repeat=-1</code>、<code>--gtest_break_on_failure</code>运行多次来复现。</p>

<h4 id="临时禁用某个单测disabled_">临时禁用某个单测：<code>DISABLED_</code></h4>

<p>可以使用<code>DISABLED_</code>前缀来跳过某项测试：</p>

<pre><code class="language-C++">TEST_F(DISABLED_BarTest, DoesXyz) { ... }
TEST_F(BarTest, DISABLED_DoesXyz) { ... }
</code></pre>

<p>DISABLED 之后，单测日志会输出 DISABLED 的单测数量：</p>

<p><img src="/media/gtest/disable.png" alt="img" /></p>

<p>之后在修理单测过程中，可以使用 <code>--gtest_also_run_disabled_tests</code> 或者 <code>--gtest_filter</code> 来执行被 DISABLED 的单测。</p>

<p>相比于把整段单测代码全部注释掉，加一个 DISABLED_ 前缀的 diff 更少，而且后续可以直接运行。</p>

<h3 id="53-输出日志">5.3 输出日志</h3>

<p>std::cout 输出的日志会直接展示在终端。</p>

<div class="ant-alert ant-alert-info">
  <p class="mb-1">💡 建议：<strong>能用 EXPECT 就不要写 std::cout</strong></p>

  <ul>
    <li>如果 cout 的日志是确定性的，那么应该写成断言。</li>
    <li>如果是 debug 用的，那么在写完单测后应该删除。</li>
    <li>如果期望单测失败时打印，那么应该放在 <code>EXPECT_CALL()... &lt;&lt; ...</code> 后面，而不是直接输出。</li>
    <li>除此之外，这些日志没有任何意义，只会刷屏，没有保留的必要。</li>
  </ul>

</div>

<h3 id="54-使用-gdb-运行和调试程序">5.4 使用 GDB 运行和调试程序</h3>

<p class="ant-alert ant-alert-info">🔗 <strong>GDB</strong> <strong>快速入门 / 速查手册</strong>：<a href="https://imageslr.com/2023/gdb.html">https://imageslr.com/2023/gdb.html</a></p>

<p>GDB 也是研发基本功之一。使用 GDB 断点调试的效率远高于<code>加日志+重新编译单测</code>，但大部分人依然使用后面这种调试方式，原因可能是认为 GDB 的上手成本太高。但实际上，GDB 入门只需要 3 分钟。这里罗列 GDB 的基本使用姿势，足够覆盖大部分单测场景。上面高亮块里也提供了一个速查手册。</p>

<ol>
  <li>进入 GDB，同时加载单测程序：
    <pre><code class="language-Shell">  gdb ./path/to/unit_test
</code></pre>
  </li>
  <li>加载动态链接库：
    <pre><code class="language-Shell">  set env LD_LIBRARY_PATH=...
</code></pre>
  </li>
  <li>运行单测：<code>r</code>。如果要运行指定单测，加 <code>--gtest_filter</code> 参数：
    <pre><code class="language-Shell">  r --gtest_filter=FooTest.bar_method
</code></pre>
  </li>
  <li>打断点：<code>b</code>。比如：
    <pre><code class="language-Shell">  b 文件名:行号
  b prime/src/auction/validator/frame/validator.cpp:52
</code></pre>
  </li>
  <li>从断点处继续运行：<code>c</code></li>
  <li>逐行执行：<code>n</code></li>
  <li>打印变量：<code>p 变量名</code></li>
  <li>查看 core 栈：<code>bt</code></li>
</ol>

<h2 id="六单测编写规范">六、单测编写规范</h2>

<p class="ant-alert ant-alert-info">💡 单测代码也需要经过 Code Review。单测代码和线上代码同等重要。</p>

<h3 id="61-目录结构文件与命名规范">6.1 目录结构、文件与命名规范</h3>

<h4 id="单测的目录结构要和源码的目录结构一致-强制">单测的目录结构，要和源码的目录结构一致 <code>[强制]</code></h4>

<p>单测文件的路径名，等价于源码的文件名加上 <code>_test</code> 后缀。</p>

<p>目的在于：让写单测的人能很快定位是否已经有这个文件或这个类的单测，让新增代码更聚合，避免写重复单测。</p>

<pre><code class="language-JSON">// bad
src/
  common/
    item_data.cpp
  frame/
    request_context.cpp
unittest/
  item_data_test.cpp // 这里直接平铺在 unittest 目录下了，和 src 目录层级不一致
  request_context_test.cpp
  
// good
src/
  common/
    item_data.cpp
  frame/
    request_context.cpp
unittest/
  common/
    item_data_test.cpp
  frame/
    request_context_test.cpp
</code></pre>

<h4 id="testsuitetestcase-命名规范-建议">TestSuite、TestCase 命名规范 <code>[建议]</code></h4>

<p>TestSuite 建议命名为被测试的类名加上 <code>Test</code> 后缀：</p>

<pre><code class="language-C++">// bad
TEST(MyTest, foo) {...}

// good
TEST(RequestContextTest, foo) {...}
</code></pre>

<p>TestCase 建议命名为被测试的函数名，不要随意起名，也不需要增加不必要的前缀：</p>

<pre><code class="language-JSON">// bad
TEST(RequestContextTest, test_uav) {
    ASSERT_EQ(ctx-&gt;init_uav_to_group_bid(), 1);
}

// good
TEST(RequestContextTest, init_uav_to_group_bid) { // 不需要加 test_ 前缀
    ASSERT_EQ(ctx-&gt;init_uav_to_group_bid(), 1);
}
</code></pre>

<p>GTest 生成的类名是带下划线的，所以上面这些名字建议用驼峰形式。</p>

<h3 id="62-写有用的单测而不只是通过单测覆盖率卡点">6.2 写有用的单测，而不只是通过单测覆盖率卡点</h3>

<h4 id="禁止写无用单测-强制">禁止写无用单测 <code>[强制]</code></h4>

<p>经典问题：“<strong>假单测</strong>”。为了通过单测覆盖率卡点、便只是在单测里执行了一下新增函数，但<strong>不检测其返回值，没有任何断言逻辑</strong>。之前遇到过有同学写了几百行单测，reviewer 从头看到尾，居然一行 EXPECT 都没有，（╯‵□′）╯。</p>

<p>还有一种场景是“<strong>蹭</strong><strong>单测</strong>”：新增了一个分支逻辑，引入了一坨逻辑，但只是在某个已有单测里，把这分支的控制参数打开了，完全没有自己构造输入去覆盖新增逻辑。这样即使覆盖率也能达标，也属于无用单测。</p>

<h4 id="测试不符合预期的边界情况而不是只测试符合预期的情况-建议">测试不符合预期的边界情况，而不是只测试符合预期的情况 <code>[建议]</code></h4>

<p>单测的目的之一在于测试程序的鲁棒性，即当输入不符合预期时，是否能正确处理。比如一个 <code>stoi</code> 函数 —— 将字符串转成整数。在构造输入时，最基本的是 <code>123</code> 这种合法字符串，此外还应当构造 <code>0.9999</code> (小数)、<code>123abc</code> (含非法字符) 等非法输入，以及 <code>1781234123412341234</code> 这种合法但越界的输入。</p>

<h3 id="63-写优雅的可理解的易于维护的单测代码风格与注释">6.3 写优雅的、可理解的、易于维护的单测：代码风格与注释</h3>

<h4 id="不要用-stdcout-输出变量值改为用-assert--expect-检查-强制">不要用 std::cout 输出变量值，改为用 <code>ASSERT</code> / <code>EXPECT</code> 检查 <code>[强制]</code></h4>

<p>能用 EXPECT 就不要写 std::cout：</p>

<ul>
  <li>如果 cout 的日志是确定性的，那么应该写成断言。</li>
  <li>如果是 debug 用的，那么在写完单测后应该删除。</li>
  <li>如果期望单测失败时打印，那么应该放在 <code>EXPECT_CALL()... &lt;&lt; ...</code> 后面，而不是直接输出。</li>
  <li>除此之外，<strong>这些日志没有任何意义</strong>，只会刷屏，没有保留的必要。</li>
</ul>

<pre><code class="language-C++">// bad 
std::cout &lt;&lt; "ads_size = " &lt;&lt; rsp.ads.size() &lt;&lt; std::endl; // 这一行多此一举
EXPECT_EQ(rsp.ads.size(), 1);

// good
EXPECT_EQ(rsp.size(), 1); // 这一行在检测失败时，会打印 rsp.size() 的值
EXPECT_EQ(rsp.size(), 1) &lt;&lt; rsp.ads.debug_string() &lt;&lt; std::endl;  // 可以在检测失败时，打印更多 debug 日志
</code></pre>

<h4 id="不要直接写数值要写清楚这个数字是怎么算的-建议">不要直接写数值，要写清楚这个数字是怎么算的 <code>[建议]</code></h4>

<p>直接写一个数字 <code>2965</code>，其他人并不知道这个数字是怎么算出来的，后续有问题也不好排查。</p>

<p>写出这个数字的计算过程，<strong>映射到代码分支上</strong>，其他人好看懂。这也是白盒化单测的表现之一。</p>

<pre><code class="language-C++">// bad
params.alpha = 2;
params.beta = 2.5;
ASSERT_EQ(params.get_score(), 2965); // 这 2965 咋算的？

// good
params.alpha = 2;
params.beta = 2.5;
ASSERT_EQ(params.get_score(), 2 * 2.5 * 593); // alpha * beta * ctx.bid

// good: 把变量名直接注释在字面量后面
ASSERT_EQ(params.get_score(), 2 /* alpha */ * 2.5 /* beta */ * 593 /* ctx.bid */);
</code></pre>

<h4 id="使用大括号分隔缩进不同的-test-case-建议">使用大括号分隔、缩进不同的 Test Case <code>[建议]</code></h4>

<p>一个 <code>TEST(Foo, Bar){...}</code> 就是一个 Test Case。考虑到构造输入有成本，通常一个 <code>TEST(Foo, Bar)</code> 里会反复修改输入，构造多个 case，测试不同的执行流程。这里建议<strong>用大括号分隔不同的 case</strong>，整体更条理。另一个好处在于：<strong>每个变量的生命周期仅限于大括号内</strong>。这样就可以反复使用相同的变量名，而不用给变量名编号。</p>

<pre><code class="language-C++">// bad
TEST(Foo, bar) {
    Context ctx1;
    params.enable_refresh = true;
    ASSERT_EQ(ctx1-&gt;is_enable_fresh(), true);
    
    Context ctx2;
    params.enable_refresh = false;
    ASSERT_EQ(ctx2-&gt;is_enable_fresh(), false);
}

// good
TEST(Foo, bar) {
    // case 1: enable = true
    {
        Context ctx;
        params.enable_refresh = true;
        ASSERT_EQ(ctx-&gt;is_enable_fresh(), true);
    }
    
    // case 2: enable = false
    {
        Context ctx;
        params.enable_refresh = false;
        ASSERT_EQ(ctx-&gt;is_enable_fresh(), false);
    }
}
</code></pre>

<p>此外，如果待测函数十分复杂，建议拆分多个 <code>TEST(Foo, Bar){...}</code>，避免 Test Case 代码膨胀。比如：</p>

<pre><code class="language-C++">// 待测函数
int foo(Ad ad) {
    if (!ad)
        return -1;
    switch(ad.pricing) {
        case CPT:
            ...
        case GD:
            ...
    }
}
// 输入为空
TEST(Foo, IsNil) {
    ...
}

// 输入是 CPT 广告
TEST(Foo, IsCpt) {
    ...
}

// 输入是 GD 广告
TEST(Foo, IsGd) {
    ...
}
</code></pre>

<h4 id="正确使用-assert-和-expect-前缀-建议">正确使用 <code>ASSERT</code> 和 <code>EXPECT</code> 前缀 <code>[建议]</code></h4>

<ul>
  <li>前者在校验失败时会直接终止，后者则会继续运行。</li>
  <li>如果某个判断不通过时会影响后续步骤 ，需要使用 <code>ASSERT</code>。常见的是空指针，或者数组访问越界。
    <blockquote>
      <p>如果某个 EXPECT 失败会导致后续一连串 EXPECT 失败，那么第一个 EXPECT 应该换成 ASSERT。这就像编译时的报错信息，往往只有第一个是有用的，其他错误都只是刷屏。</p>
    </blockquote>
  </li>
  <li>其他情况，可以使用 <code>EXPECT</code>，尽可能多测试几个用例。</li>
  <li>
    <p>此外，如果修改了某个字段的<strong>目的是影响某个函数的返回值，那么最好补一行</strong> <strong><code>ASSERT</code></strong>。好处显而易见：代码即注释；且在查单测 bug 的时候，这些断言能够预先排除一些问题。</p>

    <pre><code class="language-C++">// bad
req.type = Type::foo;  // 其他人看不懂这一行的目的是什么
EXPECT_EQ(req.get_value(), 1);
  
// good
req.type = Type::foo;
ASSERT_TRUE(context.is_foo());  // 这里表明，上一行是为了影响代码里这个判断函数的结果
EXPECT_EQ(req.get_value(), 1);
</code></pre>
  </li>
</ul>

<h4 id="解除对外部逻辑的依赖--耦合-建议">解除对外部逻辑的依赖 / 耦合 <code>[建议]</code></h4>

<ul>
  <li>如果被测代码里用到了某个全局变量：
    <ul>
      <li>Bad：从请求入口开始执行全部代码、间接构造该变量。这样太黑盒了。</li>
      <li>Good：直接就地构造变量，然后赋值到全局字段上。</li>
    </ul>
  </li>
  <li>如果被测代码里调用了某个函数：
    <ul>
      <li>Bad：想办法构造外部函数的输入，以此来影响其返回结果。这样会导致被测函数与外部函数耦合 —— 需要看外部函数的实现逻辑，且如果后续外部函数改动了，当前函数的单测可能会不通过。</li>
      <li>Good：使用 GMock 劫持该函数，在单测里控制其返回结果。完全不需要关心外部函数的实现。</li>
    </ul>
  </li>
</ul>

<h4 id="为单测补充详细的注释-建议">为单测补充详细的注释 <code>[建议]</code></h4>

<p>单测写出来必须的白盒的、可理解的、可维护的。如果不补充注释，其他人根本看不懂这些单测在测试什么逻辑，也无法确保其有效，后续修单测也很痛苦。</p>

<p>为单测补充注释时，重点要说明「这些赋值对应了哪个分支条件」，目标是让其他人扫一眼源码就能知道这些单测在测试哪些逻辑。</p>

<pre><code class="language-C++">// bad
req.type = Type::foo;
req.from = "localhost";
EXPECT_EQ(ctx.get_value(), 5);

// good：补充注释
req.type = Type::foo;  // is_foo()
req.from = "localhost";  // is_local_req()
EXPECT_EQ(ctx.get_value(), 5);  // 本地请求，默认值是 5

// best：代码即注释
req.type = Type::foo;
ASSERT_TRUE(ctx-&gt;is_foo());
req.from = "localhost";
ASSERT_TRUE(ctx-&gt;is_local_req());
EXPECT_EQ(ctx.get_value(), 5);  // 本地请求，默认值是 5
</code></pre>

<h3 id="64--写稳定的单测">6.4  写稳定的单测</h3>

<h4 id="mock-所有-io不要依赖外部数据-强制">Mock 所有 IO，不要依赖外部数据 <code>[强制]</code></h4>

<p>单测里<strong>禁止访问外部服务</strong>，最好是整个单测能够断网。</p>

<p>之前遇到的实际 case：</p>

<ul>
  <li>单测依赖线上服务，导致必须在一台线上环境的容器里才能启动单测。</li>
  <li>单测依赖了线上 redis 里的测试数据，过了半年后数据过期了，线上单测突然挂了。</li>
</ul>

<h2 id="参考文档">参考文档</h2>

<p><a href="http://google.github.io/googletest/primer.html">Gtest 官方手册 (Google Test Primer)</a> ，以及部门内的分享。</p>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[一、前言]]></summary></entry><entry><title type="html">🚀【工程】服务性能优化入门与实践指南</title><link href="https://imageslr.com/2023/service-performance-optimization.html" rel="alternate" type="text/html" title="🚀【工程】服务性能优化入门与实践指南" /><published>2023-04-09T18:00:00+08:00</published><updated>2023-04-09T18:00:00+08:00</updated><id>https://imageslr.com/2023/service-performance-optimization</id><content type="html" xml:base="https://imageslr.com/2023/service-performance-optimization.html"><![CDATA[<h2 id="一前言">一、前言</h2>

<p>在后端场景中，服务是一种提供特定功能的模块或系统，通过 REST API、RPC 等方式对外提供接口。服务可以独立运行，也可以和其他服务共同协作，构成一个庞大的系统。常见的服务有鉴权服务、搜索服务、数据库服务、广告召回服务等。</p>

<p>服务是整个系统的重要组成部分，为前端应用和其他上游服务提供支持，必须保证稳定可靠。现代服务通常需要应对高并发的请求、处理大规模的数据。随着业务和架构复杂度的增加，性能问题也会随之出现。这不仅会影响用户体验，也可能影响整个系统的稳定性。因此，服务性能优化显得尤为重要。通过优化服务性能，一方面可以降低延迟，保障服务的高可用性，提升用户体验，另一方面可以减少 CPU、内存等硬件资源的消耗，节约成本。</p>

<p>在这篇文章中，我们将围绕「服务性能优化」展开讨论，从代码、系统、架构等层面，探索服务性能优化的最佳实践。</p>

<h2 id="二相关术语">二、相关术语</h2>

<h3 id="衡量服务性能的指标">衡量服务性能的指标</h3>

<ul>
  <li><strong>延时 Latency</strong>：分成客户端和服务端两个视角，客户端即调用方，服务端即被调用方。<em>客户端延时</em>，表示调用方从发出请求到收到服务响应所需的时间。<em>服务端延时</em>，表示服务从收到请求到发出响应所需的时间。前者比后者多出两次网络传输、以及序列化 / 反序列化的时间。客户端延时通常有一个上界，这是因为客户端会设置超时时间。而服务端延时没有这样的上界，因为服务端无法感知客户端是否已经超时结束。显然，延时越低越好。</li>
  <li><strong>吞吐量 Throughput</strong>：服务在一段时间内处理请求的能力，单位通常是 QPS (Queries Per Second) 或 TPS (Transactions Per Second)。QPS 指每秒钟能够处理多少个查询请求，常用于数据库、搜索引擎等场景。TPS 指每秒钟能够完成多少个事务或操作，通常用于交易系统、支付系统等场景。在保证稳定性的前提下，吞吐量越高越好。</li>
  <li><strong>错误率 Error Rate</strong>：服务出现错误的请求数占总请求数的百分比。错误率直观展示了服务的稳定性，越低越好。</li>
  <li><strong>资源使用率</strong>：运行服务的主机或容器上各种系统资源的使用率，包括 CPU、内存、磁盘和网络等。不同服务对于资源的需求不同，例如 CPU 密集型服务更注重 CPU 的使用率，内存密集型服务则更注重内存的使用率。资源使用率越高，说明服务的负载越大，可能导致服务响应变慢、稳定性降低。因此，各项资源使用率越低越好。某些场景下可以用 A 资源兑换 B 资源，比如某个定制版的 Golang 编译器通过优化内存管理模型，用冗余的内存来兑换 CPU，效果是 (CPU <font color="orange">50%</font>, MEM <font color="green">10%</font>) → (CPU <font color="green">30%</font>, MEM <font color="orange">60%</font>)。对于 CPU 密集型服务来说，这个内存到 CPU 的兑换比是很划算的。</li>
</ul>

<h3 id="slosla">SLO、SLA</h3>

<p>SLO 和 SLA 也是服务性能优化中两个常见的概念：</p>

<ul>
  <li><strong>SLO</strong> (Service Level Objective)：服务水平目标，通常是一个数值或范围，比如稳定性达到 99.9%，即 3 个 9，表示一年内的停机时间最多为 8 小时 45 分钟、一个月内的停机时间最多为 43.2 分钟。注意，「SLO 达到 3 个 9」和「错误率低于 0.1%」并不是等价的。</li>
  <li><strong>SLA</strong> (Service Level Agreement)：服务水平协议，是由服务提供方和服务使用方达成的一份协议，约定了服务提供方应该达到的最小服务水平，若达不到应有补偿。</li>
  <li>SLO 是为了满足 SLA 而制定的。SLO 可以看作是 SLA 的内部指标，用于衡量服务是否符合 SLA 中约定的服务水平要求。</li>
</ul>

<h3 id="avgpct50pct99">avg、pct50、pct99</h3>

<p>当我们观测服务性能指标时，通常会查看一个<strong>统计值</strong>，比如所有请求的<em>平均</em>延时、集群中所有主机的<em>平均</em> CPU 使用率等。</p>

<ul>
  <li><code>avg</code> (平均值) 是所有数据的算术平均值，可以帮助我们快速了解服务性能的整体水平，但是不够准确，容易被异常值影响。</li>
  <li><code>pct50</code> (中位数) 是位于所有数据最中间的一个值。和 <code>avg</code> 相比，<code>pct50</code> 更稳定，不易受异常值影响。适用于数据分布不均匀、有异常值或者极值的场景。</li>
  <li><code>pct99</code> (百分位数) 是位于所有数据第 99% 位置的值，比如 100 个请求中的前 99 个请求，它们的延时都比 <code>pct99</code> 小，只有最后 1 个请求的延时比 <code>pct99</code> 大。<code>pct99</code> 可以帮助我们快速发现一些问题，比如存在大包体、慢查询等长尾请求，或者集群中有异常实例。</li>
  <li>类似的，还有 <code>pct90</code>、<code>pct999</code> 等指标。</li>
</ul>

<p>总之，<code>pct{n}</code> 反映了数据的分布情况，有助于我们了解服务在极端情况下的性能表现。实际场景下，我们需要同时关注 <code>avg</code>、<code>pct50</code>、<code>pct99</code> 等指标，以获取更全面的性能数据。</p>

<h3 id="基准测试压力测试">基准测试、压力测试</h3>

<p><strong>基准测试 (Benchmarking)</strong> 是一种衡量系统性能的标准化方法。基准测试常用来验证性能优化效果：首先在系统上运行一系列测试程序，保存性能指标结果；然后在代码或硬件环境变化之后，再执行一次基准测试，以确定那些变化对性能的影响。</p>

<p><strong>压力测试 (Stress Testing)</strong> 通过增加系统负载，测试系统在极端情况下的表现。压力测试可以帮助发现系统的性能瓶颈。常用的压测工具有 Apache JMeter、LoadRunner 等。</p>

<h2 id="三分析服务性能问题的工具">三、分析服务性能问题的工具</h2>

<h3 id="日志和监控">日志和监控</h3>

<ul>
  <li>
    <p>收集数据：通过日志和监控记录服务运行过程中的信息。这些信息既要包含时间戳、接口名、IP 地址、请求延时等<strong>通用信息</strong>，也要包含服务内部的<strong>具体数据</strong>，如每个子步骤的执行耗时、请求传入的数据量级等。</p>

    <div class="ant-alert">
      <p>监控数据一般用 Metrics 框架来收集。一个知名的 Metrics 框架是 <a href="http://opentsdb.net/">OpenTSDB</a>。</p>

      <p>Metrics 框架提供了<strong>存储时序数据</strong>和<strong>对时序数据进行聚合查询</strong>的功能：</p>
      <ul>
        <li>时序数据：对于某一项数据，记录其在不同时间点的值。绘制出来是横轴为时刻、纵轴为值的折线图。</li>
        <li>聚合查询：支持多种聚合方式 (avg、sum、p99 等) 和多种查询条件。</li>
      </ul>

      <p>Metrics 框架提供了通用的 API，可以在应用程序中收集各种类型的指标，比如 counter、timer 等。Metrics 数据可以展示在 Grafana 看板中。</p>

    </div>
  </li>
  <li>
    <p>展示数据：对收集到的日志和监控数据进行处理，可视化地展示其中的各项关键指标。常用的可视化工具是 <a href="https://grafana.com/">Grafana</a>。</p>

    <p><img src="/media/perf/image-20230405210259360.png" alt="image-20230405210259360" /></p>

    <p class="caption">图片来源：<a href="https://play.grafana.org/d/000000012/grafana-play-home?orgId=1">Grafana Playground</a></p>
  </li>
  <li>
    <p>分析数据：观察指标的 <code>avg</code>、<code>pct99</code> 等分位数，分析是否有长尾请求或单点异常；观察指标随时间的变化情况，定位出现性能异常的时间；同时观察多项指标，发现指标之间的关联关系，比如某时刻请求量上涨，导致 CPU 利用率上涨，进而导致服务稳定性下降。</p>
  </li>
</ul>

<p>下面是几个分析数据的实际案例。</p>

<p><strong>(1) 根据日志发现单点异常：</strong></p>

<p>假设我们从服务集群上下载了一批请求日志：</p>

<pre><code class="language-plaintext">192.168.0.1 - - [23/Sep/2021:14:45:32 +0800] "GET /api/v1/users?page=1&amp;limit=20 HTTP/1.1" 200 3567 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36" 0.504
192.168.0.2 - - [23/Sep/2021:14:45:33 +0800] "POST /api/v1/login HTTP/1.1" 200 256 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36" 1.267
192.168.0.3 - - [23/Sep/2021:14:45:34 +0800] "DELETE /api/v1/user/123 HTTP/1.1" 204 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36" 0.873
192.168.0.4 - - [23/Sep/2021:14:45:35 +0800] "PUT /api/v1/user/123 HTTP/1.1" 200 1343 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36" 0.901
192.168.0.5 - - [23/Sep/2021:14:45:36 +0800] "GET /api/v1/products?id=1234 HTTP/1.1" 200 4382 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36" 0.702
...
</code></pre>

<p>每行日志对应了一个请求，日志的第一列是处理该请求的服务主机 IP 地址，最后一列是处理该请求的耗时，单位是秒。我们可以这样统计每台主机处理请求耗时的 <code>avg</code> 指标：</p>

<pre><code class="language-shell">awk '{ips[$1]++; total[$1]+=$NF} \
END {for (ip in ips) { \
        avg=total[ip]/ips[ip]; \
        n=ips[ip]; \
        printf("%-15s requests: %-5d avg_time: %.3fs\n", ip, ips[ip], avg); \
    } \
}' log.txt | sort -k3nr
</code></pre>

<p>以上命令执行后，会输出每个 IP 地址的请求数量和平均耗时，并按平均耗时从大到小排列。输出格式如下：</p>

<pre><code>192.168.0.2    requests: 1011     avg_time: 2.267s
192.168.0.4    requests: 1103     avg_time: 0.901s
192.168.0.3    requests: 1021     avg_time: 0.873s
192.168.0.5    requests: 1007     avg_time: 0.702s
192.168.0.1    requests: 1097     avg_time: 0.504s
</code></pre>

<p>不难发现， <code>192.168.0.2</code> 的平均耗时远大于其他主机。最简单的处理办法是重启或迁移它。</p>

<p>但上面从日志统计耗时的流程很繁琐。实际场景中，一般使用 Metrics 框架收集单个请求的耗时、每台主机的 CPU 利用率等指标，然后在 Grafana 中展示。</p>

<p><strong>(2) 根据监控发现单点异常：</strong></p>

<p>通过 Metrics 和 Grafana 可以更实时、更直观地发现单点异常。比如下图是一个服务单机 QPS 的 Grafana 看板，当有一条曲线远远高于 / 低于其他曲线时，说明对应的主机有单点异常。在这个场景下，很有可能是集群负载均衡器的问题。</p>

<p><img src="/media/perf/image-20230405220119360.png" alt="image-20230405220119360" /></p>

<h3 id="火焰图">火焰图</h3>

<p>火焰图是一种性能分析工具，它以可视化的方式展示系统中的函数调用层级和执行时长。</p>

<p><img src="/media/perf/image-20230405224650600.png" alt="image-20230405224650600" /></p>

<p class="caption">图源：<a href="http://openresty.org/download/user-flamegraph.svg">http://openresty.org/download/user-flamegraph.svg</a></p>

<p>火焰图是一张形如火炬的 SVG 图片。火焰图上的每个矩形代表了一个函数的执行过程，其宽度表示执行时间的长短。矩形从下向上表示函数的调用层次，底部是外层函数，顶部是被调用的函数。矩形颜色没有含义，只是为了便于区分。</p>

<p>显然，<strong>矩形的宽度越宽，该函数的执行时间就越长，表明该函数可能存在性能问题</strong>。我们需要寻找火焰图中最宽的矩形，针对性地优化代码。</p>

<p>生成火焰图时，首先需要使用 <code>perf</code> 或 <code>DTrace</code> 等命令，收集一份包含函数执行堆栈的数据报告。然后可以使用 Brendan Gregg 开发的 <a href="https://github.com/brendangregg/FlameGraph">FlameGraph</a> 或者 Google 开发的 <a href="https://github.com/google/pprof">pprof</a> 等工具，根据收集到的数据生成火焰图。最后可以使用 <a href="https://github.com/spiermar/d3-flame-graph">d3-flame-graph</a> 等工具，将静态的 SVG 文件转换成动态的 HTML 文件，以便深入分析。</p>

<p><img src="/media/perf/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f6c34314a4d6a426178725a7731627170692f67697068792e676966.gif" alt="img" /></p>

<p class="caption">图源：<a href="https://github.com/spiermar/d3-flame-graph">https://github.com/spiermar/d3-flame-graph</a></p>

<p>火焰图不仅可以用来分析 CPU 热点，也可以用来排查<strong>内存泄漏问题</strong>。这里需要使用某些内存分配追踪工具，记录内存的分配和释放情况，然后基于这些数据生成内存火焰图。内存火焰图的矩形块颜色是绿色的，每个矩形块的宽度表示该函数内部分配的字节数。</p>

<p><img src="/media/perf/image-20230405231152705.png" alt="image-20230405231152705" /></p>

<p class="caption">图源：<a href="https://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html">Memory Leak (and Growth) Flame Graphs - Brendan Gregg</a></p>

<p>最后介绍一下<strong>差分火焰图</strong>。差分火焰图可以对比不同时间段的两张火焰图的差异，以观察哪些函数的资源开销发生了变化。差分火焰图的形状和第二张火焰图相同，矩形块的颜色表示该函数资源开销 (占比) 的差异，红色代表增长，蓝色代表减少。</p>

<p><img src="/media/perf/zfs-flamegraph-diff.svg" alt="Differential Flame Graphs" /></p>

<p class="caption">图源：<a href="https://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html">Differential Flame Graphs - Brendan Gregg</a></p>

<p>差分火焰图可以用来<strong>定量分析</strong>某项性能优化工作是否有效，比如优化了一个热点函数后，应该能从差分火焰图上看到该函数的 CPU 开销有显著减少。此外，差分火焰图也可以用来排查<strong>内存泄漏问题</strong>，比如在一台发生内存泄漏的机器上，每隔一段时间采集一份内存数据报告，然后生成内存差分火焰图，便可以很直观地看出增长的内存来自哪里。</p>

<h3 id="perf">Perf</h3>

<p>Perf 是 Linux 操作系统中一个强大的性能分析工具，可以用来追踪 CPU、内存和 I/O 等方面的性能问题。它的原理是利用 Linux 内核提供的系统调用接口，跟踪和记录各种事件的性能数据，并输出到文本文件中。Perf 命令可以和火焰图工具结合使用 —— 前者收集数据，后者可视化展示。</p>

<p>使用 <code>perf record</code> 命令，将程序的 CPU 执行情况记录到 <code>perf.data</code> 文件中：</p>

<pre><code class="language-plaintext">perf record -p {pid} sleep 30
</code></pre>

<p>上面的命令表示采集指定 <code>pid</code> 的进程，持续 30s。可能的输出：</p>

<pre><code>$ ./perf record -p 59 sleep 30
Lowering default frequency rate from 4000 to 1000.
Please consider tweaking /proc/sys/kernel/perf_event_max_sample_rate.
[ perf record: Woken up 55 times to write data ]
[ perf record: Captured and wrote 21.482 MB perf.data (462814 samples) ]
</code></pre>

<p>使用 <code>perf report</code> 命令，可视化地查看和分析数据。默认加载当前目录的 <code>perf.data</code> 文件：</p>

<pre><code>perf report
</code></pre>

<p>一个可能的数据样例如下，从中我们可以看到每个函数执行占用的 CPU 百分比：</p>

<pre><code class="language-plaintext"># Samples: 100K of event 'cycles:u'
# Event count (approx.): 1000000
#
# Overhead  Command      Shared Object                  Symbol
# ........  .......  .................  ..............................
#
    38.02%  my_prog  libfoo.so.1.2.3   /usr/lib64/libfoo.so.1.2.3
              9.21%  Foo::bar()
              8.08%  Foo::baz()
              7.12%  Foo::qux()
              6.61%  Foo::quux()
              4.48%  Annex::foo()
              2.22%  Annex::bar()
              0.30%  Annex::baz()
              0.01%  std::string::operator[](unsigned long)
              0.01%  std::operator+(std::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt; const&amp;, char const*)
              0.01%  Annex::qux()

    24.41%  my_prog  libbar.so.4.5.6   /usr/lib64/libbar.so.4.5.6
             13.05%  Bar::foo()
              6.89%  Bar::bar()
              4.70%  Bar::baz()
</code></pre>

<p>加载数据后，按 <code>/</code> 可以搜索函数名，会从高到低展示不同线程中该函数的 CPU 占比。</p>

<p>一般来说，我们可以很快通过 <code>perf report</code> 或火焰图定位到哪个函数是热点。接下来需要在机器指令级别深入分析产生性能热点的原因。在某个函数名上回车，可以进入该函数，<strong>查看每条机器指令的执行开销</strong>。以下是一个可能的数据报告：</p>

<pre><code class="language-plaintext">------------------------------------------------
 Percent |   Source code &amp; Disassembly of noploop
------------------------------------------------
         :   int main(int argc, char **argv)
         :   {
    0.00 :    8048484:       55                      push   %ebp
    0.00 :    8048485:       89 e5                   mov    %esp,%ebp
[...]
    0.00 :    8048530:       eb 0b                   jmp    804853d &lt;main+0xb9&gt;
         :                           count++;
   14.22 :    8048532:       8b 44 24 2c             mov    0x2c(%esp),%eax
    0.00 :    8048536:       83 c0 01                add    $0x1,%eax
   14.78 :    8048539:       89 44 24 2c             mov    %eax,0x2c(%esp)
         :           memcpy(&amp;tv_end, &amp;tv_now, sizeof(tv_now));
         :           tv_end.tv_sec += strtol(argv[1], NULL, 10);
         :           while (tv_now.tv_sec &lt; tv_end.tv_sec ||
         :                  tv_now.tv_usec &lt; tv_end.tv_usec) {
         :                   count = 0;
         :                   while (count &lt; 100000000UL)
   14.78 :    804853d:       8b 44 24 2c             mov    0x2c(%esp),%eax
   56.23 :    8048541:       3d ff e0 f5 05          cmp    $0x5f5e0ff,%eax
    0.00 :    8048546:       76 ea                   jbe    8048532 &lt;main+0xae&gt;
[...]
</code></pre>

<p>从中可以看到，<code>cmp</code> 指令占用了大量的 CPU 时钟周期，原因是它位于一个循环体中。</p>

<p>下面是另一份数据报告，对应了一段「在哈希表中查找关键字」的代码：</p>

<pre><code class="language-plaintext">    0.00 :    xor    %edx,%edx
    1.80 :    mov    $rdx,$r8
         :    _ZNKSt10_HashtableI1St4pair__equal_toI1ESt4hash_20_Default_
    0.03 :    mov    (%rcx),%rax
    0.03 :    mov    (%rax,%r8,8),%rax
   18.60 :    test   %rax,%rax
    ...
   14.78 :    cmp    0x8(%rbx),%r14
    ...
</code></pre>

<p class="ant-alert">💡 上面的 _ZNKSt10_xxx 是一个 <a href="https://en.wikipedia.org/wiki/Name_mangling">C++ 符号名</a>。使用 <code>c++filt</code> 命令可以将其转换为人类可读的形式。</p>

<p>从中可以看出，访存指令的开销很大，这表明哈希表在查找过程中经常失败。对应的优化手段有调整哈希函数、改进哈希冲突解决策略等，以减少哈希表的 miss 率。</p>

<p><strong>按照类似的思路，我们可以利用 Perf 命令，在指令级别分析某个函数成为性能热点的原因</strong>。比如：某指令在循环中被频繁执行、某指令涉及访存操作、某指令依赖某些暂不可用的数据 (如锁)、某指令本身是一个多周期指令等。<strong>针对不同的问题，需要采用不同的优化方案。</strong></p>

<p>最后，Brandan Gregg 有一篇非常详细的 <a href="https://www.brendangregg.com/perf.html">Perf 命令使用指南</a>，涵盖了  CPU 统计、事件分析、内核跟踪等话题，配合火焰图，基本可以排查出任意性能问题。建议深入阅读原文，此处不再展开。</p>

<h3 id="pprof">pprof</h3>

<p><a href="https://github.com/google/pprof">pprof</a> 是 Golang 官方提供的性能分析工具，可以生成 CPU、内存等多种类型的 Profiling 数据，支持以可视化的方式展示。pprof 内置了火焰图、函数调用图、表格等多种展示方式。</p>

<p>对于 Golang 服务的性能优化，建议使用 pprof，或者 <a href="pkg/profile">pkg/profile</a> 等开源库。</p>

<h2 id="四与性能优化相关的基础知识">四、与性能优化相关的基础知识</h2>

<p>后面几节将从不同层面讨论服务性能的优化手段。在此之前，有必要先了解一些基础知识。这些知识可以帮助我们更深入地理解程序性能优化的原理和方法，从而更有效地进行性能优化。</p>

<h3 id="并行与并发">并行与并发</h3>

<p>并行 (Parallelism) 和并发 (Concurrency) 都是计算机处理多个任务的方式：</p>

<ul>
  <li><strong>并行</strong>是指在同一时刻，同时执行多个任务，这通常依赖多个处理器或核心。</li>
  <li><strong>并发</strong>是指在一段时间内，通过任务的交替执行来同时处理多个任务。并发在宏观上是多任务同时运行，但是在任意时刻只有一个任务被处理。</li>
</ul>

<p>实际场景中，可以通过多进程、多线程、协程等技术实现并发，通过向量化、GPU 计算等技术实现并行，从而充分利用 CPU 资源，减少空闲时间，提高程序性能。</p>

<h3 id="指令级并行">指令级并行</h3>

<p>CPU 通常具有多个执行单元 (如整数单元、浮点数单元)，可以同时执行多个指令，这种技术称为指令并行 (Instruction-level parallelism, ILP)。以下是一些相关机制：</p>

<ol>
  <li>
    <p><strong>流水线 Pipeline</strong>：将 CPU 执行指令的过程划分为多个独立的阶段，通常包括取指令、译码、执行、访存、写回等，然后使用不同的硬件单元来并行执行不同阶段的指令。流水线可以提高 CPU 的效率，但如果遇到数据依赖或分支预测错误等问题，会导致流水线停顿。</p>
  </li>
  <li>
    <p><strong>乱序执行 Out-of-Order Execution (OOO)</strong>：在 CPU 中使用重排序缓冲区来缓存乱序执行的指令结果，再将结果按照原有的顺序提交给 CPU。</p>
  </li>
  <li>
    <p><strong>预取 Prefetch</strong>：提前将下一条指令所需的数据从内存加载到 CPU 缓存中，避免因内存访问延迟而导致的指令停顿。</p>
  </li>
  <li>
    <p><strong>动态指令调度 Dynamic Instruction Scheduling</strong>：使用指令调度器动态地调整指令的执行顺序，优化指令的执行流程。动态指令调度通常在流水线中进行，通过分析先前已执行的指令，来决定下一个要执行的指令是哪一个，以避免潜在的数据冲突和分支预测错误。</p>
  </li>
  <li>
    <p><strong>分支预测 Branch Prediction</strong>：if-else 语句、for 循环等分支指令，在执行时会根据条件跳转到不同的代码块。由于其跳转目标不确定，CPU 可能会浪费很多时间在等待分支跳转的过程中。为了解决这个问题，CPU 使用分支预测机制，在执行分支指令之前预测下一个跳转的目标指令，并进行预取。如果预测错误，CPU 需要重新执行正确分支的所有指令。</p>
  </li>
</ol>

<h3 id="局部性原理">局部性原理</h3>

<p>CPU 访问内存的时间比执行指令要长得多。因此，CPU 内部通常拥有<strong>多级缓存</strong>，如 L1 缓存、L2 缓存、L3 缓存等。缓存越靠近 CPU，访问速度越快，但容量越小；相反，缓存的级别越高，容量越大，但速度越慢。</p>

<p><strong>局部性原理</strong>指出，在计算机程序执行过程中，访问的数据和指令通常集中在空间上相邻的位置 (空间局部性)，且会在一段时间内被反复使用 (时间局部性)。因此，CPU 可以通过预取等技术将需要访问的数据和指令提前载入到 CPU 高速缓存中，以降低访存延迟的影响。</p>

<p>当 CPU 访问内存时，它并不是仅仅把单个字节或单个字从内存中读取到缓存，而是以块为单位，一次性载入多个连续字节。这个单位称为<strong>缓存块</strong> (Cache Line)，其大小通常是 64 或 128 字节。如果 CPU 访问的数据和指令都集中在一个缓存块中，那么就可以一次性载入缓存，避免多次访问内存。</p>

<p>因此在编写代码时，应该尽可能地减少访问内存的次数，使用数组、结构体等数据结构，避免不规则的内存访问模式，充分利用缓存。</p>

<h3 id="编译器优化">编译器优化</h3>

<p>编译器会对源代码进行各种优化，以使生成的汇编代码更加高效。常见的优化手段有：</p>

<ol>
  <li>指令重排序：将程序中的语句按照一定的顺序进行排列，减少指令之间的相关性，提高指令并行度和 CPU 流水线效率。</li>
  <li>常量折叠：在编译期间计算出常量表达式的结果，减少程序运行时的计算量。比如 <code>i = 320*200*32</code> 会直接代替为 <code>2,048,000</code>，而不是生成两个乘法指令。</li>
  <li>常量传播：替换表达式中已知常量，这是一个持续传播的过程，会和常量折叠交错使用。</li>
  <li>公共子表达式消除：识别重复的表达式，计算出结果并缓存，减少重复计算。</li>
  <li>强制内联：将函数的代码直接嵌入调用者的代码中，减少函数调用的开销。</li>
  <li>消除死代码：识别程序中不会执行的代码，并将其从程序中删除，减少程序的大小。</li>
  <li>循环优化：对循环代码进行代码重排、循环展开、循环移位等操作，减少指令数量。</li>
  <li>向量化：利用 CPU 的 SIMD 指令集，将程序中的标量计算转换为向量计算。</li>
  <li>尾调用优化：当函数调用位于函数体尾部的位置时，这种函数调用称为尾调用。对于尾调用函数，CPU 可以不保留外层函数的调用记录，直接用内层函数取代。</li>
</ol>

<p>在编写代码时，应注意代码的结构和风格，给予编译器相关提示，以方便编译器进行优化。</p>

<p class="ant-alert">🔗  扩展阅读：<a href="https://oi-wiki.org/lang/optimizations/#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96-tail-call-optimization">编译优化 - OI Wiki</a>、<a href="https://ucsd-pl.github.io/cse231/wi20/project.html">CSE 231 - LLVM Project</a>、<a href="https://releases.llvm.org/12.0.0/tools/polly/docs/Architecture.html">LLVM 循环优化器 Polly 架构</a></p>

<h3 id="数据结构和算法">数据结构和算法</h3>

<p>选择合适的数据结构和算法是提高程序性能的关键。</p>

<p>比如，C++ 提供了 <code>unordered_set</code> 和 <code>set</code> 来存储键值对。<code>unordered_set</code> 使用了哈希表实现，不保证元素的有序性，但是插入和查找的平均时间复杂度为 O(1)。<code>set</code> 使用红黑树实现，保证元素的有序性，但插入和查找的时间复杂度为 O(log n)。因此，如果需要有序地遍历元素，应该使用 <code>set</code>。如果需要高效地插入和查找元素，应该使用 <code>unordered_set</code>。</p>

<p>再比如，少量数据查询在不在，使用哈希表就可以实现。但海量数据查询在不在，位图或布隆过滤器可能是更合适的方式。</p>

<p>优化算法也可以降低程序的时间复杂度。比如使用快速排序代替冒泡排序，又或者在搜索过程中加入一些条件判断来<strong>剪枝</strong>、以及引入<strong>启发式搜索</strong>，提高搜索效率。</p>

<h2 id="五-代码层面的优化">五、 代码层面的优化</h2>

<p class="ant-alert ant-alert-warning">💡 下文主要描述了 C++ 的优化方法。</p>

<h3 id="使用静态分析工具">使用静态分析工具</h3>

<p>静态代码分析工具可以在不执行程序的情况下，发现潜在的代码问题，并给出优化建议。常见的 C++ 静态代码分析工具有 Clangd、Cppcheck、Coverity 等。我使用的是 Clangd，它提供了 <a href="https://github.com/clangd/vscode-clangd/">VS Code 插件</a>，能自动标识出不安全或低效率的代码，并给出 Quick fix 建议。</p>

<h3 id="减少函数调用">减少函数调用</h3>

<p>函数调用会消耗时间和空间，可以使用宏定义和 inline 函数来内嵌代码。但如果代码过长，会降低编译期和运行期的性能。对于那些非常短小或者频繁调用的函数，可以用 inline 优化。</p>

<h3 id="避免频繁创建和销毁对象">避免频繁创建和销毁对象</h3>

<ul>
  <li>通过引用或指针传递参数，而不是通过值拷贝。</li>
  <li>for 循环里的 <code>auto foo</code> 改成 <code>const auto&amp; foo</code>。</li>
  <li>正确使用 <code>std::move()</code>，避免对象拷贝。</li>
</ul>

<h3 id="编写局部性原理友好的代码">编写局部性原理友好的代码</h3>

<ul>
  <li>使用连续的存储结构。比如使用数组代替链表，使用 vector 代替 set 等。</li>
  <li>按照行优先遍历数组，而不是按照列优先。</li>
</ul>

<h3 id="使用高效的数据结构和算法">使用高效的数据结构和算法</h3>

<ul>
  <li>string_view 和 span 是 C++ 的两个标准库，类比 string 和 vector。区别在于它们对外提供<strong>只读</strong>的数据，多个对象可以<strong>共享</strong>底层的内存，避免操作时的内存拷贝。</li>
  <li>使用 flat_map、flat_set 等代替 map 和 set。前者使用了连续的内存空间存储键值对，相比于后者使用离散空间存储，遍历和随机访问的速度更快，但插入和删除操作变慢。</li>
  <li>使用 <a href="https://github.com/facebook/folly/blob/main/folly/container/F14.md">f14 hash_table</a> 代替 unordered_set。f14 在性能上有显著提升，尤其是在处理小数据集时的性能表现更佳。</li>
  <li>使用 <a href="https://maimai.cn/article/detail?fid=1763908187&amp;efid=un0Q11B47Clj6k_hM06PfA">sonic-cpp</a> 代替 <a href="https://rapidjson.org/">rapidjson</a>。sonic-cpp 利用向量化 (SIMD) 指令、优化内存布局和按需解析等关键技术，极大地提升了序列化、反序列化和增删改查的性能。</li>
  <li>使用 PB 或 Thrift 等二进制数据格式，代替 JSON、XML 等文本格式。优点是序列化 / 反序列化效率更高，生成的数据更小。缺点是配置成本高，生成的数据人类不可读，</li>
</ul>

<h3 id="深入编译器优化">深入编译器优化</h3>

<p class="ant-alert ant-alert-info"><strong><a href="https://godbolt.org">https://godbolt.org</a></strong> 是一个在线网页，可以实时将 C++ 代码编译成汇编指令，展示汇编指令和源码的对应关系，以及运行编译产物。支持 clangd、gcc 等多种编译器，支持自定义编译选项和添加外部依赖库。非常适合调试简单代码，或者深入分析编译过程。</p>

<ul>
  <li>打开 O2 或者 O3 等优化选项。O2 启用了许多常见的优化，如函数内联、循环展开和常量传播等。O3 在 O2 的基础上进一步优化代码，例如使用更高级的寄存器分配算法和更好的循环优化，但可能会导致编译时间变长。</li>
  <li>使用 <code>const</code>、<code>constexpr</code>、<code>consteval</code>、<code>constinit</code> 等关键字：
    <ul>
      <li><code>constexpr</code> ：表达式、函数、变量可以在编译期计算得到结果</li>
      <li><code>consteval</code>：函数必须在编译期计算得到结果</li>
      <li><code>constinit</code>：变量必须在编译期完成初始化</li>
    </ul>
  </li>
  <li>为不会抛异常的函数添加 <code>noexpect</code> 关键字。比如为移动构造函数加上此关键字，那么 vector 的 <code>push_back</code> 函数将调用移动构造函数，而不是默认的拷贝构造函数。</li>
  <li>提高分支预测成功率：
    <ul>
      <li>使用 <code>[[likely]]</code>、<code>[[unlikely]]</code> 修饰分支，提示编译器分支的进入概率。</li>
      <li>使用 <code>[[assume]]</code> 修饰表达式，提示编译器该表达式在运行时的结果必定为真。</li>
      <li>将数据排序，保证按顺序遍历时，前 50% 数据进入 A 分支，后 50% 数据进入 B 分支 (<a href="https://godbolt.org/z/KTdW9nbbE">Godbolt 示例</a>)。</li>
      <li>避免分支，将分支语句改为读取一个 bool 变量 (<a href="https://godbolt.org/z/KTdW9nbbE">Godbolt 示例</a>)。</li>
    </ul>
  </li>
  <li>使用 SIMD 指令集，将程序中的标量计算转换为向量计算：
    <ul>
      <li>显式使用。例如使用第三方库，封装了多种使用 SIMD 指令集的函数。或者使用编译器提供的原生向量化指令。</li>
      <li>隐式使用。编译器能够自动把某些循环代码优化为向量化指令，前提是我们要编写向量化友好的代码，比如不在循环里引入分支指令 (<a href="https://godbolt.org/z/voEEvcf9e">Godbolt 示例</a>)。</li>
    </ul>
  </li>
</ul>

<h3 id="使用-auto-fdo-优化技术">使用 Auto FDO 优化技术</h3>

<p><a href="https://gcc.gnu.org/wiki/AutoFDO/Tutorial">Auto FDO</a>  (自动反馈优化) 是一种编译器优化技术。它利用程序在运行时的性能数据，分析哪些代码路径被频繁执行，从而优化编译器生成的代码。本质上是利用真实的数据，反过来提高分支预测的成功率。实际场景中，程序的输入会经常变化，对应的代码路径分布也会变化。因此，即使是同一份代码，也需要定期重新运行 Auto FDO。</p>

<h3 id="使用并发编程技术">使用并发编程技术</h3>

<p>如多进程、多线程、协程、异步 IO 等，提高 CPU 的利用率。</p>

<h3 id="使用更高效的内存分配库">使用更高效的内存分配库</h3>

<p>如 <a href="https://github.com/jemalloc/jemalloc">jemalloc</a>、<a href="https://github.com/mkaguilera/tmalloc">tmalloc</a>。</p>

<h3 id="扩展阅读">扩展阅读</h3>

<ul>
  <li><a href="https://book.douban.com/subject/1230413/">《深入理解计算机系统 (CSAPP)》</a>：这本书涵盖了汇编指令、处理器结构、程序优化、存储器结构等与上述性能优化手段密切相关的底层知识，配套有 CMU 的公开课 <a href="https://csdiy.wiki/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/CSAPP/">CS15-213 </a>。强烈推荐学习。</li>
  <li><a href="https://book.douban.com/subject/5387403/">《Effective C++》</a>、<a href="https://book.douban.com/subject/5908727/">《More Effective C++》</a>、<a href="https://book.douban.com/subject/30178902/">《Effective Modern C++》</a></li>
  <li><a href="https://book.douban.com/subject/27666339/">《C++ 性能优化指南》</a></li>
</ul>

<h2 id="六-系统架构层面的优化">六、 系统架构层面的优化</h2>

<h3 id="优化硬件资源">优化硬件资源</h3>

<p>如使用 SSD、扩大内存等，提升磁盘读写速度。或者增加集群机器数，但是要考虑成本。</p>

<h3 id="利用缓存">利用缓存</h3>

<p>缓存并不仅指 CPU 上的 L1 / L2 / L3 缓存。理论上总是可以用速度更快的存储作为慢速存储节点的缓存。比如在内存里维护一个本地文件的缓存，或者使用 redis 作为数据库的缓存等。使用缓存时，要注意为数据设置合理的过期时间，以及选择合适的淘汰算法。</p>

<h3 id="数据库性能调优">数据库性能调优</h3>

<p>数据库调优的目的是优化数据库访问和查询的耗时，常见的手段有加索引、分库分表等，这里不作展开。</p>

<h3 id="容器化集群的优化手段">容器化集群的优化手段</h3>

<ul>
  <li>实现负载均衡，保证各个服务节点的资源利用率均匀。</li>
  <li>开启自动弹性扩缩容策略。低流量时缩容，节约成本。高流量时扩容，降低负载。</li>
  <li>潮汐集群。一般来说，在线业务在白天的流量很高，但晚上基本没有流量，冗余的机器资源正好可以给大数据、模型训练等离线任务使用。潮汐集群主要是为了节约成本，但通过资源共享，也变相地扩展了服务的计算资源。</li>
</ul>

<h3 id="限流和熔断">限流和熔断</h3>

<ul>
  <li>限流指控制服务以恒定的速率处理流量，多余的流量会被丢弃。这是为了在不确定和不稳定的流量环境中保证系统稳定运行。常见的限流算法有漏桶算法和令牌桶算法。</li>
  <li>熔断指当下游服务因访问压力过大而响应变慢或失败时，上游可以暂时切断下游调用，以保护系统整体的可用性。</li>
</ul>

<h3 id="降级机制">降级机制</h3>

<ul>
  <li>降级指在服务出现故障或者超载等情况下，主动减少或者关闭一些服务，以保证核心服务正常运行。比如秒杀系统中，只更新缓存里的库存数，然后异步更新数据库。或者推荐系统中，使用高热内容代替个性化推荐内容。</li>
  <li>对于核心服务，可以通过降低服务质量，来减轻系统压力。比如减少查询结果数量、降低返回的图片质量，减少精排服务的预估条数等。</li>
  <li>服务降级应该是一种有计划、有条理和可控的行为，在出现故障之前就需要预先规划对应的<strong>降级预案</strong>。降级预案可以手动操作，也可以自动触发。</li>
</ul>

<h3 id="个性化的降级机制">个性化的降级机制</h3>

<ul>
  <li>引入流量价值预估机制，实现请求粒度的个性化降级。比如广告系统中，可以预估当前请求的价值，低于门槛的请求自动丢弃，不召回广告，节省算力。</li>
  <li>引入剩余延时机制，根据请求在全链路的剩余延时，自动调节各个模块的降级参数。比如一个广告请求，如果前置链路耗时较短，那么可以给后续的精排模块传入更多的候选，以获得更好的点击效果。</li>
  <li>引入算力分配机制。还是以广告系统为例，在系统层面，80% 的收入是由 20% 的广告贡献的，那么可以将这 20% 的广告和剩下的 80% 的广告拆分成两个数据库，分配不同的召回条数。在用户层面，可以结合流量价值预估，减少低价值请求的预估条数，将系统的算力更多地分配给高价值请求。</li>
</ul>

<h2 id="七总结">七、总结</h2>

<p>性能优化是每个程序员的必修课。这既需要掌握相关基础知识，也需要有实际操作经验。</p>

<p>建议阅读《CSAPP》等经典书籍，并了解机器指令的原理，以更好地指导性能优化工作。线上服务在不断迭代，需要持续进行性能优化。每次性能优化后，必须通过基准测试和压力测试，验证性能优化的效果，让数据说话。</p>

<p>以上就是本文的全部内容，欢迎交流讨论。</p>

<h2 id="参考文献">参考文献</h2>

<ul>
  <li><a href="https://www.ruanyifeng.com/blog/2017/09/flame-graph.html">如何读懂火焰图 - 阮一峰</a></li>
  <li><a href="https://www.brendangregg.com/flamegraphs.html">Flame Graphs - Brendan Gregg</a></li>
  <li><a href="https://www.brendangregg.com/FlameGraphs/memoryflamegraphs.html">Memory Leak (and Growth) Flame Graphs - Brendan Gregg</a></li>
  <li><a href="https://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html">Differential Flame Graphs - Brendan Gregg</a></li>
  <li><a href="https://www.brendangregg.com/perf.html">Perf Examples - Brendan Gregg</a></li>
  <li><a href="https://oi-wiki.org/lang/optimizations/#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96-tail-call-optimization">编译优化 - OI Wiki</a></li>
  <li><a href="https://ucsd-pl.github.io/cse231/wi20/project.html">CSE 231 - LLVM Project</a></li>
  <li><a href="https://releases.llvm.org/12.0.0/tools/polly/docs/Architecture.html">LLVM 循环优化器 Polly 架构</a></li>
  <li><a href="https://engineering.fb.com/2019/04/25/developer-tools/f14/">Open-sourcing F14 for faster, more memory-efficient hash tables</a></li>
  <li><a href="https://maimai.cn/article/detail?fid=1763908187&amp;efid=un0Q11B47Clj6k_hM06PfA">性能提升 2.5 倍！字节开源高性能 C++ JSON 库 sonic-cpp</a></li>
  <li>浅析 C++ 性能优化方法和新特性 - Ads Infra Share</li>
</ul>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[一、前言]]></summary></entry><entry><title type="html">💻【程序员】使用 Netlify + Jekyll 快速搭建个人博客</title><link href="https://imageslr.com/2023/jekyll-netlify.html" rel="alternate" type="text/html" title="💻【程序员】使用 Netlify + Jekyll 快速搭建个人博客" /><published>2023-03-12T12:00:00+08:00</published><updated>2023-03-12T12:00:00+08:00</updated><id>https://imageslr.com/2023/jekyll-netlify</id><content type="html" xml:base="https://imageslr.com/2023/jekyll-netlify.html"><![CDATA[<h2 id="前言">前言</h2>

<p>在这篇文章中，我们将学习如何利用 <a href="https://www.netlify.com/">Netlify</a> + <a href="https://github.com">Github</a> + <a href="https://jekyllrb.com/">Jekyll</a>，快速零成本搭建个人博客。</p>

<p>每个技术人都应该有自己的博客。正如 Github Profile 一样，博客也是一张对外展示的名片。Github 展示了你的开源项目和编码水平，博客则展示了你的思考与技术沉淀。</p>

<p>为什么我不建议选择 CSDN、博客园、竹白等平台，或者语雀、飞书文档等个人知识库呢？一方面，每个平台有不同的调性。读者对你的印象，会受到这个平台其他作者的平均值的影响。有的平台虽然 SEO 做得很好、总是出现在搜索引擎的首位，但内容质量属实不敢恭维。出现在这样的平台上，很难保证读者不会给你的文章预设一个较低的分数。另一方面，这些平台不支持自定义主题，大家都使用统一的样式和排版风格，互相之间基本没有区分度，个人符号很难在其中得到展示。最后，有些平台是封闭的，无法被搜索引擎索引到。</p>

<p>所以，我建议申请一个<a href="#domain">独特的域名</a>，搭建一个专属于你的个人博客。在这里，我们拥有完整的自主权，可以修改主题样式、监控网站数据、分享只属于你的内容、结交志同道合的朋友。</p>

<p>当然，自建博客也有缺点，比如 SEO 差、访客数量少、缺少交互性等。但对我来说，写博客不是为了获得知名度和商业收入，而是想纯粹地记录和分享。我在搜集资料、解决问题的过程中耗费了不少时间，写一篇博客不仅可以帮助自己理清思路，还可以让知识复用。提高文章的信息量、让博客的内容有长期价值、让每位读者都有收获，这便是写作的意义。</p>

<p>总之，博客是一个值得精心打磨的作品。如果一份简历上附有独立博客的链接，我一定会想点进去看一看。如果你也有这样的想法、希望输出有价值的内容、享受书写的乐趣，那就参考下面的步骤，用 10 分钟的时间搭建一个博客吧。</p>

<h2 id="quick-start">Quick Start</h2>

<p>这一节我们将直接用  <a href="https://www.netlify.com/">Netlify</a> + <a href="https://github.com">Github</a> + <a href="https://jekyllrb.com/">Jekyll</a> 零成本搭建个人博客。简单介绍一下原理：</p>
<ul>
  <li>Jekyll 是一个<a href="#static">静态博客生成器</a>，它可以把 markdown 格式的文本内容转成静态的 HTML 页面。可以修改 CSS 来配置博客的样式和风格，网上有很多现成的<a href="http://jekyllthemes.org/">主题</a>可以使用。</li>
  <li>每个 Jekyll 项目是一个文件夹，包含了这个博客的所有内容，如 markdown 文章、图片、CSS 文件、字体资源等。</li>
  <li>Jekyll 构建的产物是纯 HTML 页面。我们把它拖到任何一个静态站点托管服务上，便可以在浏览器中访问。</li>
  <li>Github 提供了 <a href="https://docs.github.com/zh/pages/getting-started-with-github-pages/about-github-pages">Github Pages</a>。这是一个免费的静态站点托管服务，我们可以直接在某个 Github 仓库里托管 HTML 页面，然后通过  <code>&lt;username&gt;.github.io</code> 去访问。</li>
  <li>Jekyll 内置了<a href="https://docs.github.com/zh/pages/setting-up-a-github-pages-site-with-jekyll/about-github-pages-and-jekyll">对 Github Pages 的支持</a>。我们只需要把 Jellky 项目上传到 Github 仓库，再把该仓库设置为 Github Pages，就可以在每次仓库提交后，自动构建和发布。</li>
  <li><a href="https://www.netlify.com/">Netlify</a> 是一个类似于 Github Pages 的静态站点托管服务，界面操作体验更好，构建和访问速度更快。绑定一个 Github 仓库之后，Netlify 会监听该仓库的提交、自动完成构建和发布。普通用户每个月有一定的免费流量额度，作为个人博客来说完全够用了。我们可以通过 Netlify 提供的 <code>唯一id.netlify.app</code> 链接来访问网站，但既然是个人博客，最好还是绑定到一个自己的域名上。</li>
</ul>

<h3 id="一安装-jekyll-环境">一、安装 Jekyll 环境</h3>

<blockquote>
  <p>参考 Jekyll 的<a href="https://jekyllrb.com">官方文档</a>。</p>
</blockquote>

<p>先安装基本环境 Ruby 和 Ruby Gems，详见 <a href="https://jekyllrb.com/docs/installation/">https://jekyllrb.com/docs/installation/</a>。</p>

<p>然后安装 jekyll 和 bundler：</p>

<pre><code class="language-plaintext">gem install jekyll bundler
</code></pre>

<p>之后创建第一个 Jekyll 项目。你可以从零创建一个默认项目：</p>

<pre><code class="language-plaintext">jekyll new myblog
</code></pre>

<p>也可以直接复用 Github 上<a href="https://github.com/topics/jekyll-theme">公开的主题</a>，比如 <a href="https://github.com/jekyllt/jasper">jasper</a>：</p>

<pre><code class="language-bash">git clone https://github.com/jekyllt/jasper
</code></pre>

<p>最后构建网站，这会在 <code>_site</code> 目录下生成 HTML 页面，同时以 HTTP 方式提供服务：</p>

<pre><code class="language-bash"># cd jasper
# bundle add webrick
# bundle install
bundle exec jekyll serve --livereload
</code></pre>

<p>访问 <a href="http://localhost:4000">http://localhost:4000</a>，便可以看到我们的博客首页：</p>

<p><img src="/media/jekyll/image-20230312151902964.png" alt="image-20230312151902964" /></p>

<p class="ant-alert ant-alert-info">💡  给  serve 命令添加 –livereload 选项，可以在源文件有任何改变时自动刷新页面。</p>

<p class="ant-alert ant-alert-info">💡 亲测 MacOS 安装 Jekyll 环境比较麻烦，可以考虑使用现成的 docker 镜像，参考<a href="https://dev.to/stankukucka/install-jekyll-on-your-mac-with-docker-compose-file-everything-you-need-to-get-going-2alf">这篇文章</a>。</p>

<h3 id="二创建-github-仓库">二、创建 Github 仓库</h3>

<p>这里我们需要把上一步创建的 Jekyll 项目上传到 Github 仓库。比较基础，就不再赘述了。</p>

<h3 id="三配置-github-pages-可选">三、配置 Github Pages (可选)</h3>

<p class="ant-alert ant-alert-warning">💡 这一节只是为了演示 Github Pages 的功能，不建议使用它部署个人博客，推荐使用 Netlify + 自定义域名。</p>

<p>Github Pages 的<a href="https://docs.github.com/zh/pages/getting-started-with-github-pages/creating-a-github-pages-site">官方文档</a>有详细教程，下面是摘要：</p>

<ol>
  <li>
    <p>创建一个名为 <code>&lt;username&gt;.github.io</code> 的代码仓库：</p>

    <p><img src="/media/jekyll/create-repository-name-pages-20230312153656322.png" alt="创建存储库字段" /></p>
  </li>
  <li>
    <p>上传 Jekyll 项目到该仓库：</p>

    <pre><code class="language-bash"># cd jasper
# git remote add origin git@github.com:imageslr/imageslr.github.io.git
git push
</code></pre>
  </li>
  <li>
    <p>进入 Github 仓库 → Settings → Pages，配置 Jekyll Actions，如下图 ①~④：</p>

    <p><img src="/media/jekyll/image-20230312154812471.png" alt="image-20230312154812471" /></p>

    <blockquote>
      <p>如果有独立域名，也可以在上图 ⑤ 配置。</p>
    </blockquote>
  </li>
  <li>
    <p>等待几分钟，就可以通过 <a href="https://username.github.io">https://username.github.io</a> 访问博客了。</p>

    <blockquote>
      <p>如果你使用的是 jasper 主题，需要按照下图修改 <code>_config.yml</code>，才能正常加载到 CSS 资源：</p>

      <p><img src="/media/jekyll/image-20230312160345749.png" alt="image-20230312160345749" /></p>
    </blockquote>
  </li>
</ol>

<h3 id="四配置-netlify-项目">四、配置 Netlify 项目</h3>

<ol>
  <li>
    <p>访问 <a href="https://app.netlify.com">https://app.netlify.com</a>，直接使用 Github 账号登录。</p>
  </li>
  <li>
    <p>选择从 Github 导入项目 → 授予 Netlify 权限 → 安装 Netlify 应用 →  导入博客项目：</p>

    <p><img src="/media/jekyll/image-20230312161113686.png" alt="image-20230312161113686" /></p>

    <p><img src="/media/jekyll/image-20230312161205048.png" alt="image-20230312161205048" /></p>
  </li>
  <li>
    <p>等待项目首次构建完成：</p>

    <p><img src="/media/jekyll/image-20230312161406530.png" alt="image-20230312161406530" /></p>
  </li>
  <li>
    <p>然后便可以使用 Netlify 提供的 <code>唯一id.netlify.app</code> 链接来访问博客了。</p>
  </li>
</ol>

<p><br /></p>

<p>Netlify 部分功能说明：</p>

<ul>
  <li>
    <p>部署状态：</p>

    <p><img src="/media/jekyll/image-20230312161849257.png" alt="image-20230312161849257" /></p>
  </li>
  <li>
    <p>域名管理：Site settings → Domain management</p>

    <p><img src="/media/jekyll/image-20230312164404305.png" alt="image-20230312164404305" /></p>
  </li>
</ul>

<h3 id="五申请独立域名">五、申请独立域名</h3>

<p id="domain">为什么要申请独立域名？一方面，域名是我们在互联网上的符号。相比于 Github Pages 的 <code>github.io</code> 和 Netlify 的 <code>netlify.app</code>，个性化的域名有更强的个人色彩，便于读者记忆和分享。另一方面，域名是一个方便的网站定位器。当我们想要从 Netlify 迁移到其他平台时，只需要修改域名的指向记录，而不需要读者重新保存一个新的链接。</p>

<p>申请域名非常简单，只需要选择一个域名服务商、搜索喜欢的域名是否已经被注册、付费。国内的域名服务商有阿里云、腾讯云等，国外的有 GoDaddy 等。域名付费一般以年为单位，首年费用较低，但后续续费价格可能增加。国内注册域名需要备案。</p>

<p>注册域名后，可以参考上面的步骤，将域名指向 Netlify 的博客项目。</p>

<h2 id="深入讨论">深入讨论</h2>

<h3 id="博客框架">博客框架</h3>

<h4 id="静态博客-vs-动态博客">静态博客 vs 动态博客</h4>

<p id="static"><strong>静态博客</strong>生成器 (Static Site Generator) 不依赖数据库，所有博客内容都以文件的形式存储。静态博客生成器的作用是把 Markdown 格式的文本内容转成静态的 HTML 页面，需要我们自行部署。优点是轻量、易用、访问速度快、可以在本地缓存页面后离线查看。缺点是发布内容慢，需要更新本地文件 → 上传 → 部署，以及插件数量少，需要自行编码集成。</p>

<p><strong>动态博客</strong>依赖数据库，博客内容是数据库里的一个条目。优点是使用简单，能在线编写文章，有丰富的插件，自带管理后台。缺点是需要运行在服务器上，部署和维护较为繁琐。一个知名的动态博客框架是 <a href="https://wordpress.com/zh-cn/?aff=27964">WordPress</a>。</p>

<p class="ant-alert ant-alert-info">💡 我个人推荐使用静态博客框架，原因是上手简单、成本低、好维护、文章能够本地存档。</p>

<h4 id="静态博客框架对比">静态博客框架对比</h4>

<p>以下对比了几个知名的静态博客生成器。</p>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p><strong>Jekyll</strong>：<a href="https://jekyllrb.com">https://jekyllrb.com</a></p>

  <ul>
    <li>Ruby 实现的老牌博客框架。</li>
    <li>优点：网上有丰富的教程和主题；原生支持 Github Pages；支持 Sass；支持 Liquid 语法，某些语法糖很好用。</li>
    <li>缺点：装环境比较麻烦；构建速度相比于其他框架较慢；近期迭代较少。</li>
  </ul>

</div>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p><strong>Hexo</strong>：<a href="https://hexo.io/zh-cn">https://hexo.io/zh-cn</a></p>
  <ul>
    <li>Node 实现的博客框架。</li>
    <li>优点：主题众多；安装简单；构建速度快；支持 Github Pages；HTML + CSS + JS 友好；插件众多；良好的中文文档和社区支持；快速迭代。</li>
  </ul>
</div>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p><strong>Hugo</strong>：<a href="https://gohugo.io">https://gohugo.io</a></p>
  <ul>
    <li>Golang 实现的博客框架。</li>
    <li>优点：轻便，构建速度快；无需安装环境，整个工具是一个二进制文件。</li>
    <li>缺点：主题使用 Go 模板开发，需要熟悉 Go。</li>
  </ul>
</div>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p><strong>VuePress</strong>：<a href="https://vuepress.vuejs.org/zh/">https://vuepress.vuejs.org/zh/</a></p>
  <ul>
    <li>Vue 驱动的静态网站生成器。非常适合用于 Wiki、API 文档等网站类型。</li>
    <li>优点：安装简单；构建速度快；可以使用 Vue 实现自定义组件，网站的动态性更强。</li>
    <li>缺点：主题较少。</li>
  </ul>
</div>

<p class="ant-alert ant-alert-info">💡 我最终选择了 Jekyll，只是因为喜欢它的主题。从易用性来说，我更推荐 Hexo。</p>

<h3 id="使用-markdown-编写">使用 Markdown 编写</h3>

<p>Markdown 是一种用来写作的轻量级标记式语言，它使用简洁的纯文本格式来编写文档，可以转换成有效的 HTML 或 PDF 文档。Markdown 的语法十分简单，常用的标记符号不超过十个，几分钟就能掌握。可以通过这篇文章学习：<a href="https://sspai.com/post/25137">少数派：认识与入门 Markdown</a>。</p>

<p>基本上所有的静态网站生成器都是用 Markdown 写的。许多网站也支持 Markdown 语法，如 Github、少数派、石墨文档、飞书文档等。</p>

<p>我最初使用 VS Code 编辑博客的 <code>.md</code> 文件，同时打开浏览器预览效果。后来换到了 Typora，粘贴图片会更方便，也支持所见即所得。最后开发了一个和博客样式一致的 Typora 主题，就不需要再打开浏览器了。</p>

<h3 id="博客优化">博客优化</h3>

<h4 id="博客插件">博客插件</h4>

<p>以下是我的博客使用的插件。大部分插件都提供了傻瓜式的安装方法，某些插件需要有一定前端基础。这些插件都是免费的。</p>

<ul>
  <li>
    <p><a href="https://busuanzi.ibruce.info/">不蒜子</a>：极简网页计数器，两行代码搞定 PV、UV 统计。</p>
  </li>
  <li>
    <p><a href="https://giscus.app/zh-CN">giscus</a>：由 GitHub Discussion 驱动的评论系统。</p>
  </li>
  <li>
    <p><a href="https://analytics.google.com/analytics/web/provision/#/provision">Google Analytics</a>：Google 提供的站点统计工具，可以分析流量来源、所在国家、每个页面的阅读人次等。类似的工具还有<a href="https://tongji.baidu.com/web5/welcome/basic">百度网站统计</a>。</p>
  </li>
  <li>
    <p><a href="https://www.algolia.com/">Algolia</a>：网站搜索工具。本身是一个付费服务，但对开源社区提供了免费额度，需要发邮件申请，一般三个工作日会回复。申请通过后，algolia 会定时给你的网站建索引，之后在网站上添加一个搜索按钮，接入其 SDK，就可以搜索全站内容了。但如果博客的 SEO 做得不错，直接通过 <code>site: xxx.com</code> 在 Google 搜索就足够了。</p>

    <p><img src="/media/jekyll/image-20230312205507859.png" alt="image-20230312205507859" style="width:500px;margin:0" /></p>
  </li>
</ul>

<p class="ant-alert ant-alert-info">💡 建议在开发环境下关闭这些插件，避免不必要的数据污染。</p>

<h4 id="seo-优化">SEO 优化</h4>

<p>因为 Netlify 的服务器在国外，一开始百度无法索引到我的博客内容。解决办法是在<a href="https://ziyuan.baidu.com/linksubmit/index">百度站长平台</a>中主动推送网站的 sitemap：</p>

<p><img src="/media/jekyll/image-20230312205009660.png" alt="image-20230312205009660" /></p>

<p>我使用的是 jekyll，需要安装  <a href="https://github.com/jekyll/jekyll-sitemap">jekyll-sitemap</a> 插件，这会在构建博客时生成一个 <a href="https://imageslr.com/sitemap.xml">sitemap 文件</a>。之后把这个链接提交到上图的输入框中，过十几天就可以在百度搜索到网站内容了。验证方法是在百度搜索 <a href="https://www.baidu.com/s?wd=site%3A%20imageslr.com">site: imageslr.com</a>。</p>

<p>除此之外，我的博客就没有做过 SEO 优化了，也没有主动推广过。目前来看，Google 的搜索效果最好、流量最多，百度聊胜于无。每天大约 80 UV。</p>

<h4 id="性能优化">性能优化</h4>

<ol>
  <li>
    <p>获取自动优化建议。通过 <a href="https://developers.google.com/speed/pagespeed/insights/">Google PageSpeed Insights</a>、<a href="https://www.webpagetest.org/">Web Page Test</a> 等工具。</p>

    <p><img src="/media/jekyll/16333641783878.jpg" alt="16333641783878" /></p>
  </li>
  <li>
    <p>用 CDN 加速。比如从 CDN 加载 JS、CSS 文件，而不是放在博客的 <code>assets</code> 目录下。图片也可以上传到 CDN，许多 markdown 编辑器都支持配置图床，比如 Typora：</p>

    <p><img src="/media/jekyll/image-20230312211016537.png" alt="image-20230312211016537" style="width:500px;margin:0" /></p>
  </li>
  <li>减少加载的文件大小。比如使用 <code>.min.js</code> 文件、去除用不到的 CSS、使用 <a href="https://github.com/ImageOptim/ImageOptim">ImageOptim</a> 优化图片大小等。</li>
  <li>提升特定地区的访问速度。可以通过<a href="https://ping.chinaz.com">访问速度测试工具</a>，检测网站在不同国家和地区的访问速度。如果某些地区访问延迟很高或者 ping 不通，考虑在该地区部署一台实例。</li>
  <li>如果自己部署了服务器，打开 gzip 选项。</li>
</ol>

<h3 id="jeklins-自动部署-可选">Jeklins 自动部署 (可选)</h3>

<p>去年我把博客部署在了腾讯云服务器上，但因为访问量较少，就没再续费了。期间参考<a href="https://itiandong.com/2021/automating-blog-deployment-with-jenkins-and-gitee/">这篇文章</a>配置了基于「Gitee + Jenkins + 飞书机器人」的自动部署流程，记录下来，以备不时之需。</p>

<p>最终效果：我只需要往 Github 推送最新的提交，服务器就会自动拉取最新代码并构建，部署成功或失败都会给我发送一条飞书消息。</p>

<p><img src="/media/jekyll/image-20230312213846047.png" alt="image-20230312213846047" /></p>

<p>具体实现：</p>

<ol>
  <li>因为国内服务器无法访问 Github，需要创建一个 Gitee 仓库，然后参考<a href="https://zhyoch.netlify.app/2021-4/">这篇文章</a>，配置 Github 仓库自动同步到 Gitee。我的实现在<a href="https://github.com/imageslr/blog/commit/3d7717fdfbb94d296329a7a0acf3782f689c60d5">这里</a>。</li>
  <li>在<a href="https://open.feishu.cn/">飞书开放平台</a>，申请一个飞书机器人，参考<a href="https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN">这篇文档</a>获取机器人的 webhook 地址。之后就可以通过 curl 命令来给自己发送飞书消息了。</li>
  <li>服务器安装 Jenkins，创建 CI 任务。我的实现在<a href="https://github.com/imageslr/blog/blob/master/scripts/deploy.sh">这里</a>，里面集成了发送飞书消息的功能。</li>
  <li>把 Jekins 的 Webhook 地址添加到 Gitee 仓库的 WebHooks 中。参考<a href="https://itiandong.com/2021/automating-blog-deployment-with-jenkins-and-gitee/">这篇文章</a>。</li>
</ol>

<h2 id="结语">结语</h2>

<p>最后我想说，搭建博客是成本最低的操作，持续输出才是最难的。要多写精品文章、写原创内容。不要发一些可以很容易检索到的内容。提高博客文章的信息量、让博客的内容有长期价值、让每位读者都有收获。</p>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">💬【Think】关于工作的浮墨笔记</title><link href="https://imageslr.com/2023/flomo.html" rel="alternate" type="text/html" title="💬【Think】关于工作的浮墨笔记" /><published>2023-02-09T10:00:00+08:00</published><updated>2023-02-09T10:00:00+08:00</updated><id>https://imageslr.com/2023/flomo</id><content type="html" xml:base="https://imageslr.com/2023/flomo.html"><![CDATA[<p>工作了一年半载，脑海中经常会蹦出一些想法，也有些感悟心得。但写一篇文章的周期太长了，产出速度越来越慢。经验需要交流分享才能激发价值，于是打算单独开一个讨论贴，发一些工作中的碎碎念，抛砖引玉。</p>

<p>这里直接复用了 Github Discussion，在本文的评论区里可以查看。没有主题，随时记录。欢迎大家分享你的学习方法、工作心得、提效指南等。</p>

<p>原文链接：<a href="https://github.com/imageslr/blog/discussions/103">https://github.com/imageslr/blog/discussions/103</a></p>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[工作了一年半载，脑海中经常会蹦出一些想法，也有些感悟心得。但写一篇文章的周期太长了，产出速度越来越慢。经验需要交流分享才能激发价值，于是打算单独开一个讨论贴，发一些工作中的碎碎念，抛砖引玉。]]></summary></entry><entry><title type="html">💻【Linux】GDB 入门笔记</title><link href="https://imageslr.com/2023/gdb.html" rel="alternate" type="text/html" title="💻【Linux】GDB 入门笔记" /><published>2023-01-06T18:00:00+08:00</published><updated>2023-01-06T18:00:00+08:00</updated><id>https://imageslr.com/2023/gdb</id><content type="html" xml:base="https://imageslr.com/2023/gdb.html"><![CDATA[<h2 id="前言">前言</h2>

<p><a href="https://sourceware.org/gdb/">GDB</a> 全称 GNU Project debugger，是一个通用的 C / C++ 程序调试器，可以用来深入分析程序的运行过程，或者排查程序崩溃的原因。</p>

<p>GDB 主要有以下几个功能：</p>

<ol>
  <li>运行程序，随心所欲地查看程序内部状态 (如变量值、寄存器值)、控制程序的行为 (如逐行执行、反向执行等)</li>
  <li>使程序在特定位置中断，或者满足条件时才中断</li>
  <li>当程序崩溃时，查看完整现场，分析发生了什么</li>
  <li>改变程序状态 (如临时修改某个变量值)，以测试程序在不同情况下的行为</li>
</ol>

<p>在日常工作中，我经常会使用 GDB。比如线上发生 <a href="#coredump">coredump</a>，需要用 GDB 来排查；调试程序时，使用 GDB 打断点，逐行执行，效率远高于加 debug 日志。</p>

<p>GDB 和 <a href="/2021/vim.html">Vim</a> 一样，只需要学会几个简单的命令，就能解决大部分问题。但它们就像一把瑞士军刀，有丰富的功能和技巧，只有深入掌握，才能成为效率提升利器。</p>

<p>本文面向的读者是 C / C++ 程序员，主要内容包括 GDB 的基本命令、进阶用法和实践案例。目标是使读者掌握 GDB 的常见使用方法，满足日常开发所需。读者也可以将本文作为 GDB 命令的速查手册，随时查阅。</p>

<div class="ant-alert">
  <p class="mb-1"><strong>本文约定：</strong></p>
  <ul class="mb-1">
    <li>代码格式：如果没有前缀，或者前缀是 <code>$</code>，表示在 shell 执行；如果前缀是 <code>(gdb)</code>，表示在 GDB 内执行；<code>(gdb)</code> 命令后面的 <code>// xxx</code> 是注释内容，不包含在要执行的命令中。</li>
  </ul>
  <ul>
    <li>环境要求：gcc / g++，gdb。推荐<a href="/2021/computer-communicate.html#docker">使用 docker 初始化</a>。</li>
  </ul>
</div>

<h2 id="〇hello-world">〇、Hello, world</h2>

<h3 id="安装-gdb">安装 GDB</h3>

<p>本文在 Linux (CentOS) 环境下运行 GDB，读者也可以使用<a href="https://www.onlinegdb.com">网页版 GDB</a>。</p>

<p>Linux 系统可以使用包管理器安装：</p>

<pre><code class="language-shell">$ sudo apt-get update
$ sudo apt-get install gdb
</code></pre>

<p>Mac 系统可以使用 brew 安装：</p>

<pre><code class="language-shell">$ brew install gdb
</code></pre>

<p>Mac 还需要给 GDB 签名，参考 <a href="https://sourceware.org/gdb/wiki/PermissionsDarwin">GDB Wiki</a>，否则会有这样的报错：</p>

<pre><code class="language-txt">Starting program: /x/y/foo
Unable to find Mach task port for process-id 28885: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))
</code></pre>

<h3 id="使用-gdb">使用 GDB</h3>

<p>下面是一个使用 GDB 设置断点、逐行运行程序的示例。</p>

<ol>
  <li>
    <p>编写 C++ 程序：</p>

    <pre><code class="language-c++">// main.cpp
#include &lt;iostream&gt;
using namespace std;
   
void print_foo(int v) {
  int i = v + 5;
  i = i + 3;
  cout &lt;&lt; "i == " &lt;&lt; i &lt;&lt; endl;
}
   
int main() {
  int a = 0;
  a += 1;
  a += 2;
  print_foo(a);
  return 0;
}
</code></pre>
  </li>
  <li>
    <p>编译程序，添加 <code>-g</code> 选项，保留 <a href="#debug-info">debug info</a>:</p>

    <pre><code class="language-plaintext">$ g++ -g main.cpp -o example
</code></pre>
  </li>
  <li>
    <p>进入 gdb，加载二进制程序，最后一行表示符号表加载成功：</p>

    <pre><code class="language-txt">$ gdb example
GNU gdb (GDB) 12.1
Copyright ...
Find the GDB manual and other documentation resources online at:
    &lt;http://www.gnu.org/software/gdb/documentation/&gt;.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from example...
</code></pre>
  </li>
  <li>
    <p>在 <code>main()</code> 函数第一行设置一个断点，运行程序：</p>

    <pre><code class="language-txt">(gdb) b main.cpp:12
Breakpoint 1 at 0x55555555522c: file main.cpp, line 12.
(gdb) r
Starting program: /home/a.out 
Breakpoint 1, main () at main.cpp:12
12        int a = 0;
</code></pre>
  </li>
  <li>
    <p>逐行执行程序，打印变量 <code>a</code> 的值：</p>

    <p><code>next</code> 命令输出的是下一行要执行的代码。如果下一行是函数，<code>next</code> 命令会执行完整个函数，停在函数的下一行 (step over)。</p>

    <pre><code class="language-txt">(gdb) next
13        a += 1;
(gdb) p a
$3 = 1
(gdb) next
14        a += 2;
(gdb) next
15        print_foo(a);
(gdb) p a
$4 = 3
</code></pre>
  </li>
  <li>
    <p><code>step</code> 命令会进入函数，停在函数的第一行 (step into)：</p>

    <pre><code class="language-txt">(gdb) step    
print_foo (v=21845) at main.cpp:5
5       void print_foo(int v) {
</code></pre>
  </li>
  <li>
    <p><code>backtrack</code> 命令可以查看当前程序的调用栈：</p>

    <pre><code class="language-txt">(gdb) backtrace
#0  print_foo (v=21845) at main.cpp:5
#1  0x0000555555555245 in main () at main.cpp:15
</code></pre>
  </li>
  <li>
    <p><code>continue</code> 命令会执行程序，直到遇到下一个断点。这里没有下一个断点了，整个程序正常退出：</p>

    <pre><code class="language-plaintext">(gdb) continue
Continuing.
[Inferior 1 (process 1308) exited normally]
</code></pre>
  </li>
</ol>

<h3 id="命令的简写形式">命令的简写形式</h3>

<p>大部分 GDB 命令都有一个简写形式，一般是命令的首字母，比如：</p>

<ul>
  <li><code>backtrace</code> → <code>bt</code></li>
  <li><code>break</code> → <code>b</code></li>
  <li><code>continue</code> → <code>c</code></li>
  <li><code>next</code> → <code>n</code></li>
  <li><code>info</code> → <code>i</code></li>
</ul>

<p>某些命令有相同的前缀，只需要写出前几个能区分的字符，GDB 就可以识别：</p>

<pre><code class="language-plaintext">(gdb) i w    // 无法判断
Ambiguous info command "w": w32, warranty, watchpoints, win.
(gdb) i wat  // 可以识别，等于 info watchpoints
No watchpoints.
</code></pre>

<p>此外，在 GDB 中如果什么都不输入，直接回车，会重复执行上一条命令。</p>

<h3 id="命令的适用场景">命令的适用场景</h3>

<p>当应用程序异常退出时，操作系统会生成一个 <a href="#coredump">coredump</a> 文件，记录了程序退出时的所有内存状态。GDB 可以读取这个文件，查看程序退出时的变量值或者寄存器值，但是无法执行程序。即只能使用<em>静态</em>命令，如 <code>p</code>、<code>bt</code>、<code>i</code>。</p>

<p>GDB 也可以直接加载一个二进制程序并执行。在这种情况下，GDB 不仅可以随时查看程序当前的变量值或其他内存状态，还可以控制程序的运行，如设置断点、单步执行、反向执行等。即不仅可以使用<em>静态</em>命令，还可以使用 <code>r</code>、<code>b</code>、<code>c</code> 等<em>动态</em>命令。</p>

<h3 id="帮助和术语">帮助和术语</h3>

<p>在 GDB 内使用 <code>apropos {keyword}</code> 可以模糊查找某条命令：</p>

<p><img src="/media/gdb/image-20230102213313034.png" alt="image-20230102213313034" /></p>

<p>使用 <code>help {command}</code> 可以查看某个具体命令的帮助文档：</p>

<p><img src="/media/gdb/image-20230102213725055.png" alt="image-20230102213725055" /></p>

<p>此外，使用 GDB 最好了解一些计算机的基础知识：</p>

<ul>
  <li>操作系统：coredump、栈帧、线程等。</li>
  <li>组成原理：寄存器、汇编、ABI 等。</li>
</ul>

<p>部分术语的说明详见附录。</p>

<h2 id="一基本命令">一、基本命令</h2>

<h3 id="选择线程-t">选择线程: <code>t</code></h3>

<p><code>info thread</code> 可以查看当前进程的所有线程。示例程序是单线程的：</p>

<pre><code class="language-txt">(gdb) info threads
  Id   Target Id            Frame 
* 1    process 1537 "example" main () at main.cpp:15
</code></pre>

<p><code>thread</code> / <code>t</code> 可以查看当前位于哪个线程：</p>

<pre><code class="language-txt">(gdb) t
[Current thread is 1 (process 3496)]
</code></pre>

<p>在多线程程序里，可以通过 <code>t {id}</code> 切换线程，每个线程有独立的调用栈。</p>

<h3 id="查看堆栈-bt">查看堆栈: <code>bt</code></h3>

<p><code>backtrace</code> / <code>bt</code> 可以查看调用栈。调用栈展示了从 <code>main()</code> 入口到当前断点或进程退出时刻的所有函数调用路径：</p>

<pre><code class="language-txt">(gdb) bt
#0  0x0 in (unknown) at :0
#1  0x1a796e7c in foo() at main.cpp:13
#2  0x6259058 in bar() at main.cpp:17
#3  0x6bb7580 in main() at main.cpp:83
</code></pre>

<h3 id="选择栈帧-f">选择栈帧: <code>f</code></h3>

<p>每次函数调用，会创建一个独立的<a href="#stack-frame">栈帧</a>，对应上面的 <code>#0</code>、<code>#1</code>、<code>#2</code>。默认在 <code>#0</code>。</p>

<p><code>frame</code> / <code>f </code> 可以跳转到指定栈帧：</p>

<pre><code class="language-txt">(gdb) f 2
#2  bar() at main.cpp:17
17        int a = foo();
</code></pre>

<p><code>up</code> / <code>down</code> 可以向上层或下层跳转，对应编号增大或减小。</p>

<h3 id="打印变量-p">打印变量: <code>p</code></h3>

<h4 id="基本使用">基本使用</h4>

<p><code>print</code> / <code>p</code> 可以打印一个变量的值，支持数字、字符串、结构体、指针等变量类型：</p>

<pre><code class="language-txt">(gdb) p a // int a = 3;
$1 = 3
</code></pre>

<p>打印出来的值会存在名为 <code>$1</code>、<code>$2</code>、… 的变量里，后续可以直接复用：</p>

<pre><code class="language-txt">(gdb) p $1 // 等价于 p a
$2 = 3
</code></pre>

<p><code>p</code> 有一些可选参数：</p>

<ul>
  <li><code>-elements</code>：限制字符串或者数组打印的元素数量</li>
  <li><code>-max-depth</code>：限制嵌套结构体的最大打印层数</li>
  <li>…，<code>help p</code> 查看所有参数</li>
</ul>

<div class="ant-alert ant-alert-info">
  <p>💡  <code>p</code> 可以打印当前栈帧和全局作用域内的变量。如果打印变量时提示变量已经 <code>optimized</code>，可以尝试用 <code>f</code> 切换到更上层的栈帧。</p>
</div>
<h4 id="打印指针">打印指针</h4>

<h5 id="指针变量">指针变量</h5>

<p><code>p</code> 后面跟一个指针类型的变量，打印的是指针的值，即指针所指向的地址：</p>

<pre><code class="language-txt">(gdb) p b // int* b = &amp;a;
$1 = (int *) 0x7ffd3dcfa27c
</code></pre>

<p>可以用解引用运算符，打印指针指向的值：</p>

<pre><code class="language-txt">(gdb) p *b
$2 = 1
</code></pre>

<p>如果是字符串指针，<code>p</code> 会同时输出<em>指针指向的地址</em>和<em>字符串的内容</em>：</p>

<pre><code class="language-txt">p str
$3 = (char*) 0x7ffc734ff250 "hello,world"
</code></pre>

<p>如果希望只打印地址，可以使用说明符 <code>/a</code>：</p>

<pre><code class="language-txt">(gdb) p/a str
$4 = 0x7ffc734ff250
</code></pre>

<blockquote>
  <p><code>/a</code> 表示 <code>address</code>，即把变量的值以地址的形式打印。</p>
</blockquote>

<h5 id="地址字面量">地址字面量</h5>

<p><code>p</code> 默认会把十六进制的字面量看成是数字，输出一个十进制的整数：</p>

<pre><code class="language-txt">(gdb) p 0x7ffd3dcfa27c
$1 = 140725640471164
(gdb) p 140725640471164 == 0x7ffd3dcfa27c
$2 = true
</code></pre>

<p>如果想把数字解释为地址、打印地址上的内容，需要先指定变量类型，然后解引用：</p>

<pre><code class="language-txt">(gdb) p *(int*)0x7ffd3dcfa27c
$3 = 1
</code></pre>

<p>更简单的语法是 <code>{TYPE}ADDRESS：</code></p>

<pre><code class="language-txt">(gdb) p {int}0x7ffd3dcfa27c
$4 = 1
</code></pre>

<p>也可以用 <code>x</code> 命令打印地址。</p>

<h5 id="转换指针类型">转换指针类型</h5>

<p>指针的类型可以转换，以不同方式解释其指向的内存区域：</p>

<pre><code class="language-txt">// char* c = "hello, world";
(gdb) p c
$1 = (char *) 0x7ffc734ff250 "hello, world";
(gdb) p *(int*)c
$2 = 1819043176
(gdb) p {int}c
$3 = 1819043176
</code></pre>

<p>打印内存可以发现，<code>1819043176</code> 就是把 <code>h e l l</code> 四个字符解释成了一个整数：</p>

<pre><code class="language-txt">(gdb) x/w 0x7ffc734ff250    // 以 word 形式打印，4 个字节
0x7ffc734ff250:	1819043176  // 上述 4 个字符的 ASCII 码转成整数
</code></pre>

<p><code>1819043176</code> 对应的十六进制是 <code>0x6C6C6568</code>，恰好依次是 <code>l</code> , <code>l</code> , <code>e</code> 和 <code>h</code> 的 ASCII 码。</p>

<h5 id="打印结构体的字段">打印结构体的字段</h5>

<p>如果指针 <code>p</code> 指向某个结构体，可以用 <code>p ptr-&gt;field</code> 打印字段的值。</p>

<p>在 GDB 里，<code>.</code> 和 <code>-&gt;</code> 是一样的，所以无论 <code>ptr</code> 是否是指针，都可以用 <code>p.field</code> 打印字段的值。</p>

<h4 id="打印数组">打印数组</h4>

<p>语法：<code>p ELEMENT@LEN</code>。从 <code>ELEMENT</code> 的地址开始向后解释 <code>LEN</code> 大小的内存单元，内存单元的大小是 <code>sizeof(T)</code>。</p>

<h5 id="栈上数组">栈上数组</h5>

<p>如果 array 是栈上数组，可以直接 <code>p array</code>，会打印数组的所有元素：</p>

<pre><code class="language-plaintext">// int array[] = {1, 2, 3, 4};
(gdb) p array
$1 = {1, 2, 3, 4}
</code></pre>

<p>也可以 <code>p array[INDEX]@LEN</code>，从某个下标开始打印指定的长度：</p>

<pre><code class="language-txt">(gdb) p array[1]@[3] // array[1] 的类型是 int
$2 = {1, 2, 3}
</code></pre>

<p>但不能 <code>p array@LEN</code>，因为栈上数组 array 的类型是 <code>int[4]</code> 而不是 <code>int</code>：</p>

<pre><code class="language-txt">(gdb) p array@3
$3 = {{1, 2, 3, 4}, {-693741568, 32764, 1033857024, -1536906435}, {0, 0, -793505661, 32580}}
</code></pre>

<h5 id="堆上数组">堆上数组</h5>

<p>如果 array 是堆上数组，可以 <code>p *array@LEN</code>：</p>

<pre><code class="language-txt">// int* array = (int*)malloc(3 * sizeof(int));
(gdb) p *array@3 // *array 是数组的第一个元素，类型是 int
$1 = {1, 2, 3}
</code></pre>

<p>或者 <code>p array[INDEX]@LEN</code>，从某个下标开始打印：</p>

<pre><code class="language-txt">(gdb) p array[1]@3 // array[1] 的类型是 int
$2 = {2, 3, 4}
</code></pre>

<p>但不能 <code>p array</code> ，因为堆上数组 array 的类型是 <code>int*</code> 指针，值是一个地址：</p>

<pre><code class="language-txt">(gdb) p array
$3 = 0x55669a743eb0
</code></pre>

<p>也不能 <code>p array@LEN</code>，理由同上。array 是一个 <code>int*</code> 指针，保存在栈上，这里会输出栈上相邻内存的值，没有任何意义：</p>

<pre><code class="language-txt">(gdb) p array@3
$4 = {0x55669a743eb0, 0x55669a255330, 0x200000001}
</code></pre>

<p>如果只有一个地址字面量，可以把它强制转换为指针类型，然后用同样的语法打印：</p>

<pre><code class="language-txt">(gdb) p ((int*)0x55669a743eb0))[2]
$5 = 3
</code></pre>

<h4 id="格式化输出">格式化输出</h4>

<p>可以在 <code>p</code> 后面添加说明符 (specifier)，把一个变量解释为给定的类型：</p>

<pre><code class="language-txt">(gdb) p foo // int foo = 98;
$1 = 98
(gdb) p/c foo // 将 98 解释为字符
$2 = 98 'b'
</code></pre>

<p>所有说明符：</p>

<ul>
  <li>
    <p><code>p/a</code>：将变量解释为指针 address，使用十六进制打印</p>
  </li>
  <li>
    <p><code>p/c</code>：将变量解释为字符 char，打印为字符</p>
  </li>
  <li>
    <p><code>p/o</code>：使用八进制打印变量</p>
  </li>
  <li>
    <p><code>p/x</code>：使用十六进制打印变量</p>
  </li>
  <li>
    <p><code>p/u</code>：将变量解释为无符号整数 unsigned，使用十进制打印</p>
  </li>
  <li>
    <p><code>p/s</code>：将变量解释为字符串，打印输出</p>
  </li>
  <li>
    <p><code>help x</code> 查看全部：</p>

    <pre><code class="language-txt">o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), 
c(char), s(string) and z(hex, zero padded on the left)
</code></pre>
  </li>
</ul>

<h4 id="stl-容器">STL 容器</h4>

<h5 id="stdshared_ptr">std::shared_ptr</h5>

<p>直接打印：</p>

<pre><code class="language-txt">// std::shared_ptr&lt;int&gt; ptr = std::make_shared&lt;int&gt;(1);
(gdb) p ptr
$1 = std::shared_ptr&lt;int&gt; (use count 1, weak count 0) = {
  get() = 0x5596169122f0}
(gdb) p *ptr
$2 = 1
</code></pre>

<p>或者根据上面 <code>get()</code> 方法给出的地址打印：</p>

<pre><code class="language-txt">(gdb) p {int}0x5596169122f0
$3 = 1
</code></pre>

<p>或者根据 shard_ptr 内部的私有变量 <code>_M_ptr</code> 打印：</p>

<pre><code class="language-txt">(gdb) p ptr._M_ptr
$4 = 0x5596169122f0
(gdb) p *(ptr._M_ptr)
$5 = 1
</code></pre>

<h5 id="stdvector">std::vector</h5>

<p>直接打印：</p>

<pre><code class="language-txt">// std::vector&lt;int&gt; vec = {1, 2, 3, 4};
(gdb) p vec
$1 = std::vector of length 4, capacity 4 = {1, 2, 3, 4}
</code></pre>

<p>vector 也有私有变量保存了数据的实际存储位置：</p>

<ul>
  <li><code>_M_impl._M_start</code>：数组起始地址</li>
  <li><code>_M_impl._M_finish</code>：数组结束地址 (数组最后一个元素的下一个)</li>
</ul>

<p>可以根据这个指针打印：</p>

<pre><code class="language-txt">(gdb) p {int}vec._M_impl._M_start
$2 = 1
(gdb) p {int}vec._M_impl._M_start@3
$3 = {1, 2, 3}
(gdb) p ({int}vec._M_impl._M_start)[2]
$4 = 3
</code></pre>

<h5 id="stdstring">std::string</h5>

<p>直接打印：</p>

<pre><code class="language-txt">(gdb) p str
$1 = "hello,world"
</code></pre>

<p>或者根据私有变量 <code>_M_dataplus._M_p</code> 打印，其类型是 <code>char*</code>：</p>

<pre><code class="language-txt">(gdb) p str._M_dataplus._M_p
$2 = (std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;::pointer) 0x7ffc734ff250 "hello,world"
</code></pre>

<h5 id="使用插件-stl-views">使用插件 STL-Views</h5>

<p>使用 GDB 直接打印 set、stack、map 等 STL 类型是十分困难的。GDB 支持使用 python 编写 printer。<a href="https://sourceware.org/gdb/wiki/STLSupport">GDB 官网</a>提供了现成的 STL 容器的 printer，安装十分容易，开箱即用。</p>

<p>先下载源代码到 home 目录，如果终端不支持科学上网，可以网页里打开后复制内容，然后在 vim 里粘贴源代码：</p>

<pre><code class="language-txt">$ wget https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&amp;do=get&amp;target=stl-views-1.0.3.gdb -O ~/stl-views-1.0.3.gdb
</code></pre>

<p>进入 gdb，加载插件，查看帮助：</p>

<pre><code class="language-txt">(gdb) source ~/stl-views-1.0.3.gdb
(gdb) help pset
(gdb) help pmap
</code></pre>

<p>使用：</p>

<pre><code class="language-txt">(gdb) pset s
(gdb) pset s int
(gdb) pset s int 20
</code></pre>

<h4 id="如果打印内容被省略">如果打印内容被省略</h4>

<p>打印字符串的时候，如果有重复的字符，可能会被合并成一个：</p>

<pre><code class="language-txt">(gdb) p "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$1 = 'a' &lt;repeats 30 times&gt;
</code></pre>

<p>可以通过命令 <code>set print repeats 0</code> 设置为不合并：</p>

<pre><code class="language-txt">(gdb) set print repeats 0
(gdb) p "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
</code></pre>

<p>打印数组的时候，如果元素过多，中间的元素会被省略。可以通过以下设置为不省略：</p>

<pre><code class="language-txt">set print elements 0
</code></pre>

<h4 id="查看历史变量">查看历史变量</h4>

<p>通过 <code>p</code> 打印出来的值会存在名为 <code>$1</code>、<code>$2</code>、… 的变量里 (<a href="https://sourceware.org/gdb/onlinedocs/gdb/Value-History.html#Value-History">value history</a>)，后续可以直接复用：</p>

<pre><code class="language-txt">(gdb) p a
$1 = 123
(gdb) p $1 // 等价于 p a
$2 = 123
</code></pre>

<p>一些特殊的变量：</p>

<ul>
  <li><code>$</code>：最近打印的变量</li>
  <li><code>$$</code>：<code>$</code> 之前的变量，倒数第二个</li>
  <li><code>$$n</code>：最后一个变量往前的第 n 个变量，比如 <code>$$0</code> 就是 <code>$</code>， <code>$$1</code> 就是 <code>$$</code></li>
</ul>

<p>可以批量打印历史变量：</p>

<ul>
  <li><code>show values</code>：打印最后 10 个历史变量</li>
  <li><code>show values +</code>：打印刚才打印过的历史变量的后 10 个历史变量</li>
</ul>

<h3 id="打印内存-x">打印内存: <code>x</code></h3>

<p><code>x</code> 可以查看一个内存地址的值，以指定的格式打印。</p>

<pre><code class="language-txt">(gdb) x/s 0x7ffc734ff250  // 以字符串形式打印
0x7ffc734ff250:	"hello,world"
</code></pre>

<p>x 支持的<strong>格式化说明符</strong>：</p>

<ul>
  <li>
    <p><code>x/c</code>：将地址解释为字符 char，打印为字符</p>
  </li>
  <li>
    <p><code>x/o</code>：使用八进制打印变量</p>
  </li>
  <li>
    <p><code>x/x</code>：使用十六进制打印变量</p>
  </li>
  <li>
    <p><code>x/u</code>：将地址解释为无符号整数 unsigned，使用十进制打印</p>
  </li>
  <li>
    <p><code>x/s</code>：将地址解释为字符串</p>
  </li>
  <li>
    <p><code>help x</code> 查看全部：</p>

    <pre><code class="language-txt">o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), 
c(char), s(string) and z(hex, zero padded on the left)
</code></pre>
  </li>
</ul>

<p><code>x</code> 和 <code>p</code> 的区别：</p>

<ul>
  <li>
    <p>传入一个数字，<code>p</code> 会当作一个数字字面量，输出原始值的十进制；而 <code>x</code> 会当作一个地址，输出对应内存区域的值。比如：</p>

    <pre><code class="language-txt">(gdb) p 0x10    // 字面量
$1 = 16  	      // 输出十进制值
(gdb) p/x 0x10  // 以十六进制形式输出
$2 = 0x10
  
(gdb) x/s 0x10  // 这个内存地址解释为字符串
0x10 "hello, world"    
(gdb) x/c 0x10  // 把这个地址上的内容解释为单个字符
0x10:	'h'
(gdb) x/d 0x10  // 把这个地址上的内容解释为整数
0x10:	104
</code></pre>
  </li>
  <li>
    <p>传入一个指针，<code>p</code> 会输出指针的值，即一个十六进制地址；而 <code>x</code> 会输出指针指向的内存区域的值：</p>

    <pre><code class="language-txt">（gdb) p str_pointer;
$1 = 0x7ffc
  
(gdb) x/s 0x7ffc
0x7ffc "hello world"
</code></pre>
  </li>
</ul>

<p><code>x</code> 的完整语法：<code>x/FMT ADDRESS</code>，<code>F</code> / <code>M</code> / <code>T</code> 是可选的参数。</p>

<ul>
  <li><code>F</code>：一个数字，表示输出几个内存单元，默认是 1</li>
  <li><code>M</code>：格式化说明符，<code>o</code> / <code>x</code> / <code>d</code> / <code>u</code> / <code>s</code> 等</li>
  <li><code>T</code>：一个内存单元的字节数，默认是 4 个字节，可选的是 b(byte), h(halfword), w(word), g(giant, 8 bytes)</li>
  <li><code>ADDRESS</code>：一个内存地址，可以是一个字面量，也可以是一个指针类型的变量</li>
</ul>

<blockquote>
  <p>例如，<code>x/3uh 0x1234</code> 表示从内存地址 0x1234 开始，以双字节为单位，输出 3 个无符号整数。</p>
</blockquote>

<h3 id="打印类型-ptype">打印类型: <code>ptype</code></h3>

<pre><code class="language-txt">(gdb) ptype foo
type = int
</code></pre>

<h3 id="打印各种信息-i">打印各种信息: <code>i</code></h3>

<ul>
  <li><code>info locals</code>：打印当前栈帧的所有局部变量</li>
  <li><code>info args</code>：打印所有函数参数</li>
  <li><code>info threads</code>： 打印进程的线程信息</li>
  <li><code>info registers</code>： 打印当前线程的寄存器信息</li>
  <li><code>info sharedlibrary</code>：打印当前加载的动态连接库</li>
  <li><code>info proc mappings</code>：打印地址空间中的内存 map，用来确定某个地址的类型</li>
  <li><code>help info</code>：所有 info 支持的命令</li>
</ul>

<h3 id="存储变量--修改变量的值-set">存储变量 / 修改变量的值: <code>set</code></h3>

<p><code>set</code> 可以保存一个变量 (<a href="https://sourceware.org/gdb/onlinedocs/gdb/Convenience-Vars.html">convenience variables</a>)，方便后续使用：</p>

<pre><code class="language-txt">（gdb) set $foo = *object_ptr
</code></pre>

<p>查看所有存储的变量：</p>

<pre><code class="language-txt">(gdb) show convenience
(gdb) show conv  // 简写形式
</code></pre>

<p><code>set</code> 命令也可以用于在运行时修改某个变量的值：</p>

<pre><code class="language-txt">(gdb) set foo.bar = true
</code></pre>

<p>如果没有调试符号，上述命令将无法查找到变量的地址。可以手动修改变量所在的内存位置：</p>

<pre><code class="language-txt">set (char)0x7e864a2b = 1
</code></pre>

<p>修改变量值的使用场景：</p>

<ul>
  <li>临时修复某个 bug，使程序可以继续运行</li>
  <li>给变量设置不同的值，测试不同的 case</li>
</ul>

<h3 id="断点调试-b">断点调试: <code>b</code></h3>

<h4 id="设置--清除断点">设置 / 清除断点</h4>

<p>设置断点：<code>break POINT</code>，简写是 <code>b</code></p>

<pre><code class="language-txt">(gdb) b foo.cpp:14
</code></pre>

<p>设置断点的方式有<a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_28.html">多种</a>：</p>

<ul>
  <li>在当前执行位置设断点：<code>b</code>，没有任何参数</li>
  <li>函数名：<code>b function</code></li>
  <li>文件名 + 函数名：<code>b filename:function</code></li>
  <li>行号：<code>b linenum</code>，在当前文件设置断点</li>
  <li>文件名 + 行号：<code>b filename:linenum</code>，在特定文件设置断点</li>
  <li>偏移量：<code>b +offset</code> / <code>b -offset</code>，在当前栈帧执行位置的前后设置断点</li>
  <li>给汇编命令打断点：略</li>
</ul>

<p>删除断点：<code>clear</code></p>

<pre><code class="language-txt">(gdb) clear foo.cpp:14
</code></pre>

<p><code>clear</code> 的语法和 <code>break</code> 相同，需要指定要删除的断点的位置：</p>

<ul>
  <li><code>clear</code>：删除当前执行位置上的所有断点</li>
  <li><code>clear function</code>、<code>clear filename:function</code></li>
  <li><code>clear linenum</code>、<code>clear filename:linenum</code></li>
  <li><code>delete</code>：删除所有断点，简写是 <code>d</code></li>
</ul>

<p>设置临时断点：<code>tbreak</code>。参数同 <code>break</code>，命中一次后就会自动删除。</p>

<blockquote>
  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_28.html">GDB - Setting breakpoints</a></p>

  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_31.html#SEC32">GDB - Deleting breakpoints</a></p>
</blockquote>

<h4 id="停用--启用断点">停用 / 启用断点</h4>

<p>停用断点：<code>disable</code></p>

<pre><code class="language-txt">(gdb) disable      // 停用所有断点
(gdb) disable NUM  // 停用编号为 n 的断点
</code></pre>

<p>停用断点后，断点将暂时不被触发。可以通过 <code>enable</code> 命令启用断点，语法同 <code>disable</code>。</p>

<h4 id="继续运行-cont">继续运行: <code>cont</code></h4>

<p>命中断点后程序会停止运行，此时可以输入 <code>continue</code> 命令，继续运行程序。简写是 <code>cont</code>。</p>

<h4 id="查看所有断点i-b">查看所有断点：<code>i b</code></h4>

<pre><code class="language-txt">(gdb) i b
(gdb) info breakpoints
</code></pre>

<p>这会以表格的形式展示断点编号、是否是临时断点、是否 enable、断点位置等信息。</p>

<h4 id="在函数返回前中断">在函数返回前中断</h4>

<p>有时候希望在函数返回前中断，从而检查函数的返回值，或者检查函数是在哪一个 <code>return</code> 语句返回的。</p>

<p>有两种方式。一种是反向调试，先正向执行，直到函数返回，然后再反向执行，设置断点：</p>

<pre><code class="language-txt">(gdb) record
(gdb) fin
(gdb) reverse-step
</code></pre>

<p>另一种方式更通用。所有的函数无论有多少条 <code>return</code> 语句，在编译成汇编指令后，一定是只有一条 <code>retq</code> 指令。因此可以在汇编指令里找到 <code>retq</code> 所在位置打断点：</p>

<pre><code class="language-txt">int main() {
  return foo(0);
}

(gdb) disas foo  // 查看汇编
Dump of assembler code for function foo:
   0x0000000000400448 &lt;+0&gt;: push   %rbp
   0x0000000000400449 &lt;+1&gt;: mov    %rsp,%rbp
   ...
   0x0000000000400473 &lt;+43&gt;:    jmp    0x40047c &lt;foo+52&gt;
   0x0000000000400480 &lt;+56&gt;:    retq   // 这里就是函数的返回指令
End of assembler dump.

(gdb) b *0x0000000000400480  // 在 retq 指令打断点
Breakpoint 1 at 0x400480

(gdb) r  // 运行程序，直到命中断点
Breakpoint 1, 0x0000000000400480 in foo ()

(gdb) p var
$1 = 42
</code></pre>

<h3 id="监控断点-watch">监控断点: <code>watch</code></h3>

<p>GDB 可以监控一个变量，直到它被修改时才触发断点：</p>

<pre><code class="language-txt">(gdb) watch foo
(gdb) watch bar.var
</code></pre>

<p>如果想在变量被读取时中断，可以使用 <code>rwatch</code> 或 <code>awatch</code>：</p>

<ul>
  <li><code>rwatch</code>：仅当变量被读取时终端</li>
  <li><code>awatch</code>：当变量被读取或写入时中断</li>
</ul>

<p>查看所有 watchpoints：</p>

<pre><code class="language-txt">(gdb) info watchpoints
</code></pre>

<p>禁用 / 删除 watchpoints 的命令同 <code>break</code>。</p>

<blockquote>
  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_29.html#SEC30">GDB - Setting watchpoints</a></p>
</blockquote>

<h3 id="条件断点-b--if">条件断点: <code>b ... if</code></h3>

<p>常规断点 (breakpoints) 和监控断点 (watchpoints) 都可以绑定一个条件，只在满足条件时才触发断点。</p>

<p>“条件”是一个布尔表达式：</p>

<pre><code class="language-txt">(gdb) b foo.cpp:123 if bar == 1
(gdb) b foo.cpp:123 if bar == 1 &amp;&amp; foo &lt; 2
</code></pre>

<p>如果要判断两个字符串是否相等，可以使用 gdb 的内置函数 <code>$_streq</code>：</p>

<pre><code class="language-txt">(gdb) b foo.cpp:123 if $_streq(some_str, "hello_world")
</code></pre>

<blockquote>
  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_33.html#SEC34">GDB - Break conditions</a></p>
</blockquote>

<h3 id="断点命令列表-commands">断点命令列表: <code>commands</code></h3>

<p>可以通过 <code>commands</code> 命令给断点绑定一组自定义命令，当命中断点后会自动执行，如打印变量的值，或者设置另一个断点。</p>

<p>语法：先指定要绑定的断点编号，然后输入自定义命令，最后以 <code>end</code> 结束。例如：</p>

<pre><code class="language-txt">（gdb) commands 1
（gdb) p foo
（gdb) end
</code></pre>

<p>断点编号可以通过 <code>i b</code> 或 <code>i wat</code> 获取。如果不给 <code>commands</code> 传入任何编号，则默认绑定到最近触发的断点上。</p>

<p><code>commands</code> 的应用场景之一是收集信息。比如在某行代码后面插入一行 debug 日志，打印变量或调用栈。由于每次命中断点后，必须输入 <code>cond</code> 命令才会继续运行程序，因此可以在 <code>end</code> 前面加一个 <code>cont</code> 命令，这样程序便可以无需干预、自动运行：</p>

<pre><code class="language-txt">(gdb) b foo.cpp:123
(gdb) commands
(gdb) p bar
(gdb) cont
(gdb) end
</code></pre>

<p><code>commands</code> 的另一个应用场景是临时修复一个 bug，以便让程序正常运行。比如在某一行错误代码后面，给变量设置正确的值。同样要以 <code>continue</code> 命令结尾：</p>

<pre><code class="language-txt">(gdb) b foo.cpp:123
(gdb) commands
(gdb) silent  // 这个命令后面的命令不会有任何输出
(gdb) set x = y + 4
(gdb) cont
(gdb) end
</code></pre>

<blockquote>
  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_34.html#SEC35">GDB - Breakpoint command lists</a></p>
</blockquote>

<h3 id="运行程序-n--s--c--fin--u">运行程序: <code>n</code> / <code>s</code> / <code>c</code> / <code>fin</code> / <code>u</code></h3>

<ul>
  <li><code>run</code> / <code>r</code>：运行程序，直到遇到第一个断点或者运行结束</li>
  <li><code>start</code>：启动程序，临时停在 main() 的第一行</li>
  <li><code>next</code> / <code>n</code>：逐行执行，如果某一行是函数，不会进入到函数里，而是会执行完整个函数 (step over)</li>
  <li><code>step</code> / <code>s</code>：逐行执行，如果某一行是函数，会进入到函数的第一行 (step into)</li>
  <li><code>continue</code> / <code>c</code>：从断点位置继续执行，直到遇到下一个断点或者运行结束</li>
  <li><code>finish</code> / <code>fin</code>：执行到函数结束，停在 return 后的下一条语句</li>
  <li><code>until</code> / <code>u</code>：
    <ul>
      <li>不加任何参数：执行直到当前语句结束，比如在 for loop 里 <code>until</code> 会跳到 for 循环体的下一行</li>
      <li>加参数：执行直到特定位置，参数的语法同 <code>break</code>，等价于 <code>tbreak</code> + <code>continue</code></li>
    </ul>
  </li>
  <li><code>quit</code> / <code>q</code>：退出 GDB</li>
</ul>

<p>直接回车会重复上一次执行的命令，所以在单步跟踪的时候，无论是 <code>s</code> 还是 n 都可以连续敲回车继续执行。</p>

<h3 id="输出日志-set-logging">输出日志: <code>set logging</code></h3>

<p>可以把 GDB 的所有输出打印到日志里，作进一步分析。</p>

<p>需要执行这两个命令：</p>

<pre><code class="language-txt">(gdb) set logging file gdb.txt
(gdb) set logging on
copying output to gdb.txt
</code></pre>

<p>这样任何命令的输出便会写到 <code>gdb.txt</code>，前提是 shell 拥有该文件的写入权限。</p>

<p>配合以下命令，确保输出完整内容：</p>

<pre><code class="language-txt">set print repeats 0       // 否则相同的连续字符会被合并
set print elements 0      // 否则过长的数组会被省略
set height 0              // 否则如果一页显示不完，会停下来要求 continue
set width 0  
</code></pre>

<h2 id="二进阶用法">二、进阶用法</h2>

<h3 id="配置文件-gdbinit">配置文件: <code>~/.gdbinit</code></h3>

<p>像 <code>~/.vimrc</code>、<code>~/.zshrc</code> 一样，GDB 也有默认的配置文件 <code>~/.gdbinit</code>。可以把一些常用的配置、插件、自定义命令放在 <code>~/.gdbinit</code>。</p>

<p>Github 上有一些开箱即用的 <code>~/.gdbinit</code> 文件：</p>

<ul>
  <li><a href="https://github.com/gdbinit/Gdbinit/blob/master/gdbinit">https://github.com/gdbinit/Gdbinit/blob/master/gdbinit</a></li>
  <li><a href="https://github.com/cyrus-and/gdb-dashboard">gdb-dashboard</a>：可视化界面、丰富的功能</li>
  <li><a href="https://github.com/hugsy/gef">gef</a>：可视化界面、丰富的功能</li>
  <li><a href="https://github.com/pwndbg/pwndbg">pwndbg</a></li>
</ul>

<p>gdb-dashboard 使用笔记：</p>

<ul>
  <li>使用 <code>-output</code> 命令将某些组件在其他终端显示，比如终端 A 执行 gdb 命令，终端 B 显示断点、变量值、调用栈。在终端输入 <code>tty</code> 命令就可以查看当前终端的序号。</li>
  <li>介绍文章：<a href="https://zhuanlan.zhihu.com/p/435918702">https://zhuanlan.zhihu.com/p/435918702</a></li>
</ul>

<h3 id="加载插件-source">加载插件: <code>source</code></h3>

<p>GDB 可以使用 <a href="https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html">Python API</a> 来实现自定义脚本。脚本可以直接写在 <code>~/.gdbinit</code>，或者写在一个单独的文件中，然后通过 <code>source</code> 命令加载。</p>

<p>网上有很多可用的插件，比如 <a href="https://sourceware.org/gdb/wiki/STLSupport">STL views</a> 提供了一些打印 STL 容器的命令。</p>

<!-- 
### 反向调试: `reverse`



### 可视化界面 TUI

不是很重要，重点说说那些插件的实际使用吧



### Attach 到进程中: `attach`




 -->

<h2 id="三实践案例">三、实践案例</h2>

<blockquote>
  <p>TODO 待补充</p>
</blockquote>

<h2 id="附录">附录</h2>

<h3 id="学习资源">学习资源</h3>

<ul>
  <li>GDB 官网：<a href="https://sourceware.org/gdb/">https://sourceware.org/gdb/</a></li>
  <li><a href="https://sourceware.org/gdb/onlinedocs/gdb/">Debugging with GDB</a></li>
  <li><a href="https://www.brendangregg.com/blog/2016-08-09/gdb-example-ncurses.html">gdb debug full examples</a></li>
  <li><a href="https://wizardforcel.gitbooks.io/100-gdb-tips/content/index.html">100个 GDB 小技巧</a></li>
  <li><a href="https://pernos.co">https://pernos.co</a>：在线 GDB 平台</li>
</ul>

<h3 id="术语">术语</h3>

<h4 id="stack-frame">栈帧</h4>

<p>调用栈 (call stack) 被分成若干个栈帧 (stack frame)，每个栈帧包括和一次函数调用相关的所有数据：函数的参数、函数的局部变量、以及函数的返回地址等。</p>

<p>程序启动时只有一个栈帧，即 main 函数，又称<em>初始栈帧</em>或<em>最外层栈帧</em>。每次函数调用都会创建一个新的栈帧，每次函数返回时一个栈帧也会被弹出。当前执行的函数所对应的栈帧又称<em>最内层栈帧</em>。</p>

<p>GDB 给每个栈帧分配了一个数字，最内层栈帧的编号是 0，外层栈帧依次加 1。可以通过 <code>bt</code> 命令展示所有栈帧，通过 <code>f</code> 命令加上编号进入到对应的栈帧。</p>

<blockquote>
  <p><a href="https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_41.html#SEC42">GDB - Stack frames</a></p>
</blockquote>

<h4 id="coredump">Core Dump</h4>

<p>当进程崩溃时，操作系统会把进程当前的所有内存和寄存器状态信息保存到 core dump 文件中。Core dump file 是一个二进制文件，需要配合 debug info 来赋予其含义。GDB 可以读取 core dump 文件，协助分析进程崩溃的瞬间发生了什么。</p>

<p>可能会产生 core dump 文件的场景：</p>

<ul>
  <li>段错误 Segmentation Fault
    <ul>
      <li>Null Pointer Dereference (NPD)</li>
      <li>Stack Overflow / Buffer Overflow</li>
      <li>Use After Free (UAF)</li>
      <li>Double Free</li>
      <li>Out Of Memory (OOM)</li>
    </ul>
  </li>
  <li>其他一些会引起 core dump 的 signal</li>
</ul>

<blockquote>
  <p><a href="https://www.cse.unsw.edu.au/~learn/debugging/modules/gdb_coredumps/">GDB Coredumps</a></p>
</blockquote>

<h4 id="debug-info">Debug Info</h4>

<p>Debug 是编译器生成的调试用的符号表，保留了源代码的信息，如标识符名称、可执行文件中第几条机器指令对应源代码的第几行等，但并不是把整个源文件嵌入到可执行文件中。</p>

<p>gcc 或 g++ 在编译时，可以通过 <code>-g</code> 选项生成 debug info。如果没有 debug info，GDB 就无法按源码行打断点、输出变量的值、或者展示 coredump 文件中的调用栈信息。</p>

<p>DWARF 是现在操作系统 debug info 的主要标准。Debug info 保存在程序 ELF 文件的 <code>.debug_info</code> 段中。</p>

<blockquote>
  <p><a href="https://developers.redhat.com/articles/2022/01/10/gdb-developers-gnu-debugger-tutorial-part-2-all-about-debuginfo">The GDB developer’s GNU Debugger tutorial, Part 2: All about debuginfo</a></p>
</blockquote>

<p class="ant-alert ant-alert-info">📒  相关文章：<a href="/2021/vim.html">💻【Linux】Vim 学习笔记</a></p>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">💻【Linux】Vim 入门笔记</title><link href="https://imageslr.com/2021/vim.html" rel="alternate" type="text/html" title="💻【Linux】Vim 入门笔记" /><published>2023-01-05T12:00:00+08:00</published><updated>2023-06-22T00:00:00+08:00</updated><id>https://imageslr.com/2021/vim</id><content type="html" xml:base="https://imageslr.com/2021/vim.html"><![CDATA[<h2 id="-视频分享vs-code--vim">📺 视频分享：VS Code × Vim</h2>

<p>最近，我在 VS Code 中深度使用了 Vim 插件。Vim 快捷键基本代替了所有的鼠标操作，极大提升了我的编码效率。因此，我做了一期视频分享。</p>

<p>在下面的视频中，我分享了 VS Code 和 Vim 的快捷键、如何在实际编码场景中使用 Vim、以及如何配置 Vim 的快捷键映射。欢迎大家一起交流！</p>

<details>
  <summary>查看目录和时间戳</summary>
  <ul>
    <li>00:00 为什么不用 Vim 编辑器，而是要在 VS Code 中使用 Vim 插件？</li>
    <li>03:00 VS Code 的常用快捷键</li>
    <li>08:19 Vim - 移动 / Normal Mode</li>
    <li>20:15 Vim - 编辑 / Insert Mode</li>
    <li>28:29 Vim - 复制粘贴</li>
    <li>31:10 Vim - 选择 / Visual Mode</li>
    <li>36:08 Vim - 其他 / Inside &amp; Around</li>
    <li>41:43 VSCode × Vim 实际编码过程演示</li>
  </ul>
</details>

<details>
  <summary>查看思维导图</summary>
  <p><img src="/media/vim/vim.png" alt="vim" /></p>
</details>

<iframe src="//player.bilibili.com/player.html?aid=357198715&amp;bvid=BV1MX4y1b7nM&amp;cid=1159419111&amp;page=1&amp;autoplay=false" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="width:100%; aspect-ratio:16/9" id="bili-video"> </iframe>

<h2 id="为什么要学习-vim">为什么要学习 vim</h2>

<p>最近遇到在服务端编辑代码文件、查日志的场景比较多，所以想要系统学习一下 vim。</p>

<p>Vim 对于每个服务端开发人员都不陌生，这可能是我们接触最多的 Linux 软件。所有类 Unix 的系统（Linux、Mac）都安装了 vim。当我们通过终端操作文本时，vim 或许是我们唯一的选择。</p>

<p>然而，vim 的使用方式和我们所熟悉的可视化编辑器完全不同，它的的快捷键是如此奇怪，不易上手。因此除非兴趣使然，我们很少会主动学习 vim。它的上限够高，下限也足够低，只需要掌握最基本的操作：<code>↑↓←→</code>、<code>i</code>、<code>&lt;ESC&gt;</code>、<code>:wq</code>，就可以覆盖大部分使用场景。那为什么还需要再深入学习 vim 呢？</p>

<p>主要原因是：<strong>用较少的学习成本，换来较大的效率提升</strong>。Vim 常用的几个快捷键，可以在手指不离开键盘热区的情况下快速定位光标或编辑内容，这些内容的学习成本并不高。如果你开发运维的过程中和 vim 打交道的次数越来越多，掌握这些技巧可以极大的提升开发效率。即使现在没有需求，也可以提前上手这个强大的工具。</p>

<h2 id="为什么要写这篇文章">为什么要写这篇文章</h2>
<p>现有的 vim 教程 / 文章大多直接罗列完整的 vim 快捷键列表，让人不知从何下手。我认为应当先掌握最重要的、最高频的快捷键，满足日常开发所需；其他低频使用的快捷键，可以作为一个速查表按需查看，vim 的进阶用法也可以之后再深入研究。</p>

<p>因此，我尝试作为一个 vim 初学者，总结 vim 主要和次要的快捷键，同时提供一些学习 vim 的资源。</p>

<p>注意：在阅读本文时，你随时可以在终端执行 <code>vimtutor</code>，打开一个教程文本文件，尝试某个快捷键或命令。</p>

<h2 id="学习资源">学习资源</h2>
<ul>
  <li><a href="https://missing-semester-cn.github.io/2020/editors/">Missing Semester - 3. Vim 编辑器</a>：非常推荐的课程，所有内容都很有帮助</li>
  <li><a href="https://youtu.be/wlR5gYd6um0">Mastering Vim</a>：讲解 vim 里的“编程语言范式”，整体理解 vim</li>
  <li><a href="https://www.youtube.com/watch?v=ER5JYFKkYDg">The Vim Tutorial Part One - Youtube</a>, <a href="https://www.youtube.com/watch?v=tExTz7GnpdQ&amp;t=467s">Part Two</a>：看英文字幕比较吃力的话，可以直接看本文</li>
  <li>vimtutor：安装 vim 后自带的教程，在终端执行 <code>vimtutor</code> 即可打开，mac 系统下是中文文档<br />
<img src="/media/vim/16114169033178.jpg" alt="-w1439" /></li>
  <li><a href="https://vim-adventures.com/">Vim Adventure</a>：以游戏的方式学习 vim</li>
</ul>

<blockquote>
  <p>这里顺便再推荐一些可视化学习资源：</p>
  <ul>
    <li>数据结构与算法：<a href="https://www.cs.usfca.edu/~galles/visualization/Algorithms.html">Data Structure Visualization</a>、<a href="https://visualgo.net/zh">Visualgo</a></li>
    <li>正则表达式：<a href="https://regexper.com/">regexper</a></li>
    <li>Git：<a href="https://learngitbranching.js.org/?demo=&amp;locale=zh_CN">Learn Git Branching</a>（强烈推荐）、<a href="http://git-school.github.io/visualizing-git/">Visualizing Git</a></li>
  </ul>
</blockquote>

<h2 id="术语说明">术语说明</h2>

<p>vim 中有一些术语：</p>

<ul>
  <li>mode：标准模式 normal、插入模式 insert、可视模式 visual 等。</li>
  <li>verb：vim 里执行的操作，比如删除 <code>d</code>、修改 <code>c</code>、拷贝 <code>y</code>、查找 <code>f</code> 等。verb 后面需要跟一个 motion，表示该操作生效的范围。</li>
  <li>motion：vim 里移动的范围，比如向右一个字母 <code>l</code>、向下一行 <code>j</code>、向右一个单词 <code>w</code> 等。本文中有时候也称其为 “range”。</li>
</ul>

<p class="ant-alert ant-alert-info">💡 如果你对 vim 的语法感兴趣，可以阅读这一篇文章：<a href="https://learnvim.irian.to/basics/vim_grammar">https://learnvim.irian.to/basics/vim_grammar</a></p>

<h2 id="符号说明">符号说明</h2>

<p>标题的前缀：</p>

<ul>
  <li><code>[M]</code>：移动类，move</li>
  <li><code>[E]</code>：编辑类，edit</li>
  <li><code>[F]</code>：查找类，find</li>
</ul>

<h2 id="入门">入门</h2>

<h3 id="标准模式--插入模式">标准模式 / 插入模式</h3>
<ul>
  <li>标准模式（Normal Mode）：进入 vim 的默认模式，这个模式下按下任何键不会实际输入到文本中，按下 <code>:</code> 可以执行命令</li>
  <li>插入模式（Insert Mode）：在标准模式按下 <code>i</code> 进入插入模式，此时可以输入文本；按下 <code>&lt;ESC&gt;</code> 退出插入模式</li>
</ul>

<p class="ant-alert ant-alert-info">💡 除了以上两个模式，vim 还有 <a href="#visual-mode">visual mode</a>，用来选择一个范围的文本。</p>

<p>建议配置 <code>jj</code> 退出插入模式，这样左手不需要移动到最左上角去按下 <code>&lt;ESC&gt;</code>。在标准模式下执行：</p>
<pre><code>imap jj &lt;Esc&gt;
</code></pre>

<p>可以将这条命令写到 vim 的<a href="#conf">配置文件</a>中。我实际上是把 <code>jj</code> / <code>kk</code> / <code>jk</code> / <code>kj</code> 都配成了退出插入模式。</p>

<h3 id="执行命令command">执行命令：<code>:&lt;command&gt;</code></h3>
<p>按下 <code>:</code> 后输入命令，按回车执行。如 <code>:set number</code> 会显示行号。</p>

<h3 id="退出-vimq--zz">退出 vim：<code>:q</code> / <code>ZZ</code></h3>
<ul>
  <li><code>:q</code> / <code>:quit</code>：退出 vim，不作任何改动</li>
  <li><code>:q!</code>：退出 vim，丢弃已有的改动</li>
  <li><code>:wq</code>：保存更改（write）并退出（quit）vim</li>
  <li><code>ZZ</code>：等同于 <code>:wq</code>，这个快捷键输入比 <code>:wq</code> 更快，注意是大写 <code>Z</code></li>
</ul>

<h3 id="保存文件-w--w-filename">保存文件: <code>:w</code> / <code>:w &lt;filename&gt;</code></h3>
<ul>
  <li><code>:w</code>：保存更改</li>
  <li><code>:w &lt;filename&gt;</code>：保存到一个新的文件</li>
</ul>

<h3 id="m-基本移动h--j--k--l">[M] 基本移动：<code>h</code> / <code>j</code> / <code>k</code> / <code>l</code></h3>
<p>使用 <code>h</code>、<code>j</code>、<code>k</code>、<code>l</code> 而不是 <code>←</code>、<code>↓</code>、<code>→</code>、<code>↑</code>，这能够避免将手指移出键盘热区再移回来。如果有必要的话，甚至可以禁用方向键，来强制自己使用 <code>h</code>、<code>j</code>、<code>k</code>、<code>l</code>：</p>

<pre><code>map &lt;Left&gt; &lt;Nop&gt;
map &lt;Right&gt; &lt;Nop&gt;
map &lt;Up&gt; &lt;Nop&gt;
map &lt;Down&gt; &lt;Nop&gt;
</code></pre>

<h3 id="m-前往第一行--最后一行gg--g">[M] 前往第一行 / 最后一行：<code>gg</code> / <code>G</code></h3>
<ul>
  <li><code>gg</code>：前往第一行</li>
  <li><code>1G</code>：同 <code>gg</code></li>
  <li><code>G</code>：前往最后一行</li>
</ul>

<h3 id="m-前往第-n-行ngn">[M] 前往第 n 行：<code>nG</code>、<code>:n</code></h3>

<ul>
  <li><code>nG</code>：前往第 n 行。<code>1G</code> 可以前往第一行。如果希望在 vim 中显示行号，可以在标准模式下执行 <code>set number</code> 命令，也可以将这条命令写到 vim 的<a href="#conf">配置文件</a>中。</li>
  <li><code>:n</code> 也可以前往第 n 行，比如 <code>:123</code>前往第 123 行。</li>
</ul>

<h3 id="m-向右移动一个单词w--e">[M] 向右移动一个单词：<code>w</code> / <code>e</code></h3>
<ul>
  <li><code>w</code>：向右移动一个单词，光标将落在下一个单词的首字符</li>
  <li><code>e</code>：向右移动一个单词，光标将落在当前一个单词的最后一个字符</li>
</ul>

<p>在这里，一个<code>单词</code>的定义是<strong>连续的「数字+字母+下划线」</strong>，或者<strong>连续的「特殊字符」</strong>。比如 <code>hello, world!!!</code> 里包含 <code>hello</code>、<code>,</code>、<code>world</code> 和 <code>!!!</code> 四个单词。按 <code>w</code> 跳转时，会跳过单词后面的所有空白字符，落在下一个单词的开头。示例：</p>

<pre><code>↓ 光标在这里
Hello, world!
     ↑ 按下 w
    ↑ 按下 e
Hello, world!
       ↑ 按两下 w
     ↑ 按两下 e
</code></pre>

<p>类似的还有 <code>W</code> / <code>E</code>，区别在于这两个快捷键对<code>单词</code>的定义是<strong>连续的「非空字符」</strong>，即以空格作为单词的分界线。比如 <code>hello, world!!!</code> 里，包含 <code>hello,</code> 和 <code>world!!!</code> 两个单词。</p>

<h3 id="m-向左移动一个单词b">[M] 向左移动一个单词：<code>b</code></h3>
<p><code>b</code> 向左移动到前一个单词的首字符，相当于是 <code>w</code> 的逆操作。<code>b</code> 取 backwards 首字母，「单词」的定义同 <code>w</code>。</p>

<p><code>2b</code> 向左移动两个单词，<code>nb</code> 向左移动 n 个单词。</p>

<p><code>B</code> 向左移动一个单词，将「空格」作为单词的分隔符（同 <code>W</code>、<code>E</code>）。</p>

<h3 id="m-移动到前一个单词的末尾ge">[M] 移动到前一个单词的末尾：<code>ge</code></h3>
<p><code>ge</code> 移动到前一个单词的末尾，<code>gE</code> 将空格作为单词的分隔符。</p>

<h3 id="m-前往当前行第一个--最后一个字符0----_--">[M] 前往当前行第一个 / 最后一个字符：<code>0</code> / <code>$</code> / <code>_</code> / <code>^</code></h3>
<ul>
  <li><code>0</code>：前往第一个字符，可以理解成是第 0 列</li>
  <li><code>$</code>：前往最后一个字符</li>
  <li><code>_</code>：前往第一个非空字符，这在编写 python 等有缩进的代码时很有用。<code>^</code> 等价。</li>
</ul>

<h3 id="e-删除字符x--x">[E] 删除字符：<code>x</code> / <code>X</code></h3>
<ul>
  <li><code>x</code>：删除当前字符，等同于 <code>&lt;Delete&gt;</code></li>
  <li><code>X</code>：删除前一个字符</li>
</ul>

<h3 id="e-删除单词dw">[E] 删除单词：<code>dw</code></h3>
<p>「单词」的定义同 <code>w</code>，单词后面的任意多个空格将被删除。</p>

<p>类似的还有 <code>dW</code>，删除下一个空格前的单词。</p>

<h3 id="e-删除当前行dd">[E] 删除当前行：<code>dd</code></h3>
<p>略。</p>

<h3 id="e-在当前位置后面插入a">[E] 在当前位置后面插入：<code>a</code></h3>
<p><code>i</code> 在当前位置前面插入（insert），<code>a</code> 在当前位置后面插入（append）。</p>

<h3 id="e-在当前行开始--末尾插入i--a">[E] 在当前行开始 / 末尾插入：<code>I</code> / <code>A</code></h3>
<p>略。</p>

<h3 id="e-在当前行下面--上面插入新行o--o">[E] 在当前行下面 / 上面插入新行：<code>o</code> / <code>O</code></h3>
<p>插入新的空白行。</p>

<h3 id="e-改变大小写--gu--gu">[E] 改变大小写：<code>~</code> / <code>gu</code> / <code>gU</code></h3>

<ul>
  <li>
    <p><code>~</code>：将光标下的字母改变大小写。<code>3~</code> 是将光标开始的 3 个字母改变大小写。</p>
  </li>
  <li><code>gu&lt;motion&gt;</code>：指定范围的字母变成小写。比如 <code>guw</code> 是后一个单词全变成小写，<code>guj</code> 是当前整行改成小写。<code>gU</code> 则是改成大写。</li>
  <li><code>guu</code>：将当前行的字母改成小写。<code>gUU</code> 是改成大写。</li>
  <li><code>guiw</code>：将光标所在的单词改成小写。详见下文 <a href="#inside-around">inside / around</a>。</li>
</ul>

<h3 id="f-移动到下一个指定字符ftarget">[F] 移动到下一个指定字符：<code>f&lt;target&gt;</code></h3>
<p><code>ft</code> 移动到下一个 <code>t</code> 出现的位置，<code>f2</code> 移动到下一个 <code>2</code> 出现的位置。<code>f</code> 取 forward 的首字母。</p>

<p><code>F</code> 类似于 <code>f</code>，向前移动到前一个指定字符。</p>

<p><code>t</code> 类似于 <code>f</code>，只不过光标会移动到下一个指定字符<strong>之前</strong>；<code>T</code> 类似于 <code>F</code>，只不过光标会移动到前一个指定字符<strong>之后</strong>。<code>t</code> 取 until 的含义。</p>

<p>示例：</p>
<pre><code>    ↓ 光标在这里
Hello, world!
        ↑ fo
         ↑ fr
↑ Fh
       ↑ to
 ↑ Th
</code></pre>

<h3 id="e-撤销--重做u--ctrl--r">[E] 撤销 / 重做：<code>u</code> / <code>&lt;Ctrl&gt; + r</code></h3>
<ul>
  <li><code>u</code>：撤销（undo）</li>
  <li><code>&lt;Ctrl&gt; + r</code>：重做（redo）</li>
</ul>

<h2 id="中场休息vim-的一些模式">中场休息：vim 的一些模式</h2>

<h3 id="ntimes">重复 n 次操作：<code>n&lt;action&gt;</code></h3>
<p>Vim 中几乎所有操作都可以通过一个 <code>n</code> 前缀来重复 n 次：</p>

<ul>
  <li><code>5h</code> 向左移动 5 个字符。<code>5j</code> 向下移动 5 行。<code>2w</code> 向右移动两个单词，等同于按两次 <code>w</code>。</li>
  <li><code>2x</code> 删除两个字符，<code>2X</code> 向左删除两个字符。</li>
  <li><code>3fa</code> 在当前行查找第 3 次出现的 <code>a</code>，等同于按 3 次  <code>fa</code>。</li>
  <li><code>2u</code> 撤销前两步操作，等同于按两次 <code>u</code>。<code>2&lt;Ctrl&gt; + r</code> 重做被撤销的两步操作，等同于按两次 <code>&lt;Ctrl&gt; + r</code>。</li>
</ul>

<p class="ant-alert ant-alert-info">💡 移动类命令 (motion) 如 <code>h</code> / <code>w</code>，和操作类命令 (verb) 如 <code>d</code> / <code>x</code> / <code>f</code> 都支持在前面加 <code>n</code> 来重复多次。</p>

<h3 id="操作任意范围verbmotion">操作任意范围：<code>&lt;verb&gt;&lt;motion&gt;</code></h3>

<p>我们以 <code>d</code> 命令为例。<code>d</code> 可以和任意光标移动的操作结合，来删除一个<strong>范围</strong>的内容。</p>

<p>比如：<code>w</code> 是跳到下一个单词的开头，那么 <code>dw</code> 就是删除到下一个单词的开头；<code>b</code> 是跳转到前一个单词的开头，那么 <code>db</code> 就是删除到前一个单词的开头。以此类推，<code>d0</code> 是删除当前位置到行开头的所有内容，<code>dG</code> 是删除当前行到文件末尾的所有内容，<code>dgg</code> 是删除当前行到文件开头的所有内容。</p>

<p>大部分命令都支持 <code>&lt;verb&gt;&lt;n&gt;&lt;motion&gt;</code> 和 <code>&lt;n&gt;&lt;verb&gt;&lt;motion&gt;</code> 两种模式，比如 <code>d2w</code> 和 <code>2dw</code> 都是删除后两个单词。但在语义上有区别：<code>d2w</code> 表示删除 <code>2w</code> 范围的内容，而 <code>2dw</code> 表示 <code>dw</code> 命令重复 2 次。</p>

<p class="ant-alert ant-alert-info">💡 <code>d</code> / <code>c</code> / <code>y</code> 等 verb，都支持上述模式。比如 <code>c2w</code> 修改后两个单词，<code>y2j</code> 复制下两行。</p>

<h3 id="命令的小写和大写d--d-等">命令的小写和大写：<code>d</code> / <code>D</code> 等</h3>
<p>不同的方向：</p>
<ul>
  <li><code>x</code> 向右、<code>X</code> 向左</li>
  <li><code>p</code> 向下、<code>P</code> 向上</li>
  <li><code>o</code> 向下、<code>O</code> 向上</li>
  <li><code>f</code> 向右、<code>F</code> 向左</li>
</ul>

<p>更严格的条件：</p>
<ul>
  <li><code>w</code> 将特殊字符作为独立单词，<code>W</code> 只将空格作为单词分隔符</li>
  <li><code>e</code> / <code>E</code>、<code>b</code> / <code>B</code> 同理</li>
</ul>

<p>更大的范围：</p>
<ul>
  <li><code>a</code> 在当前位置后面插入、<code>A</code> 在当前行末尾插入</li>
  <li><code>i</code> 在当前位置前插入、<code>I</code> 在当前行开始插入</li>
  <li><code>d</code> 删除一个范围、<code>D</code> 删除到行末尾</li>
  <li><code>c</code> 删除一个范围、<code>C</code> 删除到行末尾，并进入编辑模式</li>
  <li><code>s</code> 删除当前字符，并进入编辑模式；<code>S</code> 删除当前整行，并进入编辑模式</li>
</ul>

<p>连续操作：</p>
<ul>
  <li><code>r</code> 替换一个字符、<code>R</code> 连续替换多个字符直到按下 <code>&lt;Esc&gt;</code></li>
</ul>

<h3 id="两个字母重复dd--cc--yy--guu">两个字母重复：<code>dd</code> / <code>cc</code> / <code>yy</code> / <code>guu</code></h3>

<p>两个 verb 字母重复，表示对当前整行操作：</p>

<ul>
  <li><code>dd</code>：删除整行</li>
  <li><code>cc</code>：删除整行，并进入编辑模式</li>
  <li><code>yy</code>：复制整行</li>
  <li><code>guu</code>：当前整行变成小写</li>
  <li><code>gUU</code>：当前整行变成大写</li>
</ul>

<h3 id="重复上次操作----">重复上次操作：<code>.</code> / <code>,</code> / <code>;</code></h3>

<ul>
  <li><code>.</code>：重复上次的编辑操作。比如执行了 <code>A123&lt;Esc&gt;</code> 在当前行尾插入 <code>123</code> 后，可以移动到下一行，按 <code>.</code> 在该行末尾插入 <code>123</code>。</li>
  <li><code>,</code> / <code>;</code>：重复当前行内的上一次 / 下一次 <code>f</code> 查找。比如在当前行按 <code>fa</code> 找到第一个 <code>a</code> 字符后，按 <code>;</code> 可以查找下一个，等价于再按 <code>fa</code>。按 <code>,</code> 是查找上一个，等价于 <code>Fa</code>。</li>
  <li><code>n</code> / <code>N</code>：按 <code>/bar&lt;Enter&gt;</code> 搜索 <code>bar</code> 字符串后，按 <code>n</code> 可以查找下一个，按 <code>N</code> 查找上一个。</li>
</ul>

<h2 id="进阶">进阶</h2>

<h3 id="e-替换一个字符r">[E] 替换一个字符：<code>r</code></h3>
<p><code>r</code>：再按下任意键，替换（replace）当前字符，等同于 <code>x</code> + <code>i</code>。示例：</p>
<pre><code>    ↓ 光标在这里
Helle, world!
# 先按 r，再按 o
Hello, world!
    ↑ 光标在这里
</code></pre>

<h3 id="e-替换连续多个字符r">[E] 替换连续多个字符：<code>R</code></h3>
<p><code>R</code>：替换连续的多个字符，按下 <code>&lt;Esc&gt;</code> 可以退出替换模式。</p>

<h3 id="e-更改一个范围的内容c">[E] 更改一个范围的内容：<code>c</code></h3>
<p><code>c</code> 取 change 的首字母，这个命令的便捷之处在于将「删除操作」和「进入编辑模式」合二为一，可以少按一个键。</p>
<ul>
  <li><code>cw</code>：更改下一个单词，等同于 <code>dw</code> + <code>i</code></li>
  <li><code>c2w</code>：更改后两个单词，等同于 <code>d2w</code> + <code>i</code></li>
  <li><code>c$</code>：更改从当前位置到行结束的所有内容，等同于 <code>d$</code> + <code>i</code></li>
</ul>

<p>和 <code>d</code> 一样，<code>c</code> 也可以和任意光标移动的操作符结合，来更改一个范围的内容。</p>

<h3 id="e-删除当前字符并进入编辑模式s">[E] 删除当前字符，并进入编辑模式：<code>s</code></h3>
<p><code>s</code> 等同于 <code>x</code> + <code>i</code>。</p>

<h3 id="e-删除当前整行并进入编辑模式s--cc">[E] 删除当前整行，并进入编辑模式：<code>S</code> / <code>cc</code></h3>
<p><code>S</code> 等同于 <code>dd</code> + <code>o</code>。<code>cc</code> 也可以删除整行，并进入编辑模式。</p>

<h3 id="e-从当前位置开始向右删除整行d">[E] 从当前位置开始向右删除整行：<code>D</code></h3>
<p><code>D</code> 等同于 <code>d$</code>。</p>

<h3 id="e-从当前位置开始向右删除整行并进入编辑模式c">[E] 从当前位置开始向右删除整行，并进入编辑模式：<code>C</code></h3>
<p><code>C</code> 等同于 <code>c$</code>，或者 <code>d$</code> + <code>a</code>，或者 <code>D</code> + <code>a</code>。</p>

<h3 id="visual-mode">[V] 可视模式选择内容：<code>v</code> / <code>V</code> / <code>&lt;Ctrl&gt; + v</code></h3>

<p>按下 <code>v</code> 进入可视模式（visual mode），然后移动光标以选择文本。可以针对选中的文本执行任意操作 (verb)，比如：</p>
<ul>
  <li>按下 <code>y</code> 可以复制选中的文本，再移动到别的位置按下 <code>p</code> 粘贴这些文本</li>
  <li>按下 <code>d</code> 可以删除选中的文本</li>
</ul>

<p><img src="/media/vim/vim-visual-mode.gif" alt="vim-visual-mode" /></p>

<p>按下  <code>ctrl + v </code> 可以进入 Visual Block Mode，选择一个矩形块里的内容：</p>

<p><img src="/media/vim/image-20230315105320258.png" alt="image-20230315105320258" style="width:400px;" /></p>

<p id="VVisual">按下大写 <code>V</code> 可以选中整行。常见的使用技巧：</p>

<ol>
  <li>按 <code>V</code> 选中整行，按 <code>j</code> 向下选中多行，然后 <code>y</code> 复制。</li>
  <li>按 <code>V</code> 选中整行，按 <code>j</code> 向下选中多行，按 <code>&lt;</code> 向左缩进，按 <code>.</code> 继续缩进。</li>
  <li>按 <code>V</code> 选中整行，按 <code>j</code> 向下选中多行，按 <code>=</code> 格式化。</li>
</ol>

<h3 id="复制下一个单词yw">复制下一个单词：<code>yw</code></h3>

<p><code>y</code> 取 yank（复制）的首字母。<code>yw</code> 复制下一个单词，<code>p</code> 可以将其粘贴（put）到指定位置。</p>

<p>事实上，<code>y</code> 和 <code>c</code>、<code>d</code> 一样，可以和任意光标移动的操作符结合，来复制一个范围的内容。比如 <code>y$</code> 将复制当前位置到行末尾的全部内容，<code>yh</code> 将复制光标前面的字符，<code>yG</code> 复制光标所在行到最后一行的所有内容。</p>

<p>最后，<code>yy</code> 复制当前行，可以和 <code>dd</code> 一起理解 —— <code>dd</code> 删除一整行，快捷键重复表示操作的是<strong>一整行</strong>，不管光标位置在哪里。第二个 <code>y</code> 和 <code>d</code> 并没有语义上的含义。</p>

<h3 id="复制当前行yy--2yy--y2j">复制当前行：<code>yy</code> / <code>2yy</code> / <code>y2j</code></h3>

<p><code>yy</code> 复制当前行，<code>p</code> 粘贴到目标位置。</p>

<p><code>nyy</code> 复制当前行往下的 n 行，包括当前行。</p>

<p><code>ynj</code> 也是复制当前行往下的 n 行。比如 <code>y2j</code> 会复制当前行和下一行。</p>

<h3 id="粘贴到下一行--上一行p--p">粘贴到下一行 / 上一行：<code>p</code> / <code>P</code></h3>

<p>如上所述，<code>p</code> 粘贴到目标位置。</p>

<p>通过 <code>dd</code> 删除某一行后，也可以按下 <code>p</code>，将删除掉的内容放置到当前光标位置下一行。注意这里是「放置」而不是「粘贴」，因为 <code>dd</code> 将被删除的行保存到了缓冲区，而 <code>p</code> 其实是将缓冲区的内容放置到当前位置，所以 <code>p</code> 取 put 的首字母，而非 paste。</p>

<p>同理，<code>yy</code> 将当前行保存到缓冲区，但不删除。这样 <code>yy</code> + <code>p</code> 就可以实现“复制-粘贴”的操作。</p>

<p>大写 <code>P</code> 粘贴到上一行。</p>

<h3 id="当前行置顶zt--zb--zz">当前行置顶：<code>zt</code> / <code>zb</code> / <code>zz</code></h3>
<p><code>zt</code> 把当前行置于屏幕顶端。<code>z</code> 字取其象形意义，模拟一张纸的折叠变形。<code>t</code> 取 top 的首字母。</p>

<p><code>zz</code> 将当前行置于屏幕中央。<code>zb</code> 将当前行置于屏幕底端，<code>b</code> 取 bottom 的首字母。</p>

<h3 id="缩进--">缩进：<code>&lt;</code> / <code>&gt;</code></h3>

<p>选中文本后，<code>&lt;</code> 是向左缩进，<code>&gt;</code> 是向右缩进。</p>

<h3 id="格式化">格式化：<code>=</code></h3>

<p>按 <code>=</code> 可以将选中的文本格式化。这个命令可以配合 <a href="#VVisual"><code>V</code></a> 使用 —— 通过 <code>V</code> 选中多行，然后按 <code>=</code> 格式化选中的文本。</p>

<h2 id="高级">高级</h2>

<h3 id="inside-around">[E] inside 和 around：<code>di*</code> / <code>da*</code></h3>

<p>对于 Vim 的删除命令 <code>d</code>，还有一类比较常用的操作是以 <code>i</code> (<code>inside</code> 或 <code>inner</code>) 和 <code>a</code> (<code>around</code>) 为后缀的命令，用于删除以当前光标所在的语法元素内部或周围的字符。比如：</p>

<ul>
  <li><code>diw</code> 和 <code>daw</code>：前者删除当前光标所在的单词，后者会删除当前光标所在的单词与后面的空格。</li>
  <li><code>di(</code> 和 <code>da(</code>：前者会删除括号内的内容，后者还会删除括号本身。类似的还有<code>di[</code>、<code>da[</code>、<code>di{</code>、<code>da{</code>等。</li>
  <li><code>dib</code> 和 <code>dab</code>：等价于 <code>di(</code> 和 <code>da)</code>。<code>b</code> 表示 bracket。</li>
  <li><code>di"</code> 和 <code>da"</code>：前者删除双引号内的内容，后者还会删除引号本身。</li>
  <li><code>dit</code> 和 <code>dat</code>：修改 tag 包围的内容。vim 会自动将 <code>&lt;tag&gt;</code> 和 <code>&lt;/tag&gt;</code> 识别为一对 tag。这在前端开发场景中很有用。</li>
  <li><code>dip</code>：删除当前整段 (paragraph) 的内容。</li>
</ul>

<p>除了 <code>d</code> 命令，<code>c</code> / <code>v</code> / <code>gu</code> 等命令也可以和 inside、around 组合。比如 <code>ciw</code> 是修改当前单词，<code>viw</code> 是选中当前单词。</p>

<p>inside、around 也可以和上面的 <a href="#ntimes">重复 n 次</a> 结合使用。比如光标位于 <code>(a * (b + c))</code> 的字符 <code>c</code> 时，按 <code>di(</code> 将删除内层括号里的 <code>b + c</code>，按 <code>d2i(</code> 将删除外层括号里的全部内容。</p>

<p>inside 和 around 命令可以大大提高操作效率，尤其是在编辑代码时。</p>

<h3 id="f-查找文档中的关键字pattern">[F] 查找文档中的关键字：<code>/&lt;pattern&gt;</code></h3>
<p><code>/</code> 从光标所在位置向后查找关键字，<code>n</code> / <code>N</code> 查找下一个 / 上一个匹配的位置。</p>

<p><code>?</code> 向前查找，不过很少使用。如果想向前查找的话，使用 <code>/</code> + <code>N</code> 就可以了。</p>

<p><code>q/</code>、<code>q?</code> 可以列出 <code>/</code>、<code>?</code> 的查找历史，上下选择，按 <code>i</code> 编辑，回车执行，<code>:q</code>退出。</p>

<p><code>&lt;pattern&gt;</code> 可以是正则表达式，比如 <code>/vim$</code> 查找位于行尾的 <code>vim</code>。查找特殊字符时需要转义，比如 <code>/vim\$</code> 查找 <code>vim$</code>。</p>

<p>在查找模式中加入 <code>\c</code> 表示大小写不敏感查找，<code>\C</code> 表示大小写敏感，比如 <code>/foo\c</code> 会查找 <code>foo</code>、<code>Foo</code> 等。默认是大小写敏感，可以执行 <code>:set ignorecase</code> 或写入<a href="#conf">配置文件</a>设置大小写不敏感为默认的查找模式。</p>

<p>查找相关命令：</p>
<pre><code>set ic // 等价于 set ignorecase
set hls is // 高亮匹配项
nohlsearch // 移除匹配项的高亮显示
</code></pre>

<h3 id="f-查找当前光标对应的完整单词--">[F] 查找当前光标对应的完整单词：<code>*</code> / <code>#</code></h3>
<p>示例：</p>
<pre><code>  ↓ 光标在这里
Hello, world!
</code></pre>

<p>此时按下<code>*</code>，将向后查找 <code>Hello</code> 这个单词。按下 <code>#</code> 是向前查找。</p>

<h3 id="f-在代码块匹配的括号之间跳转">[F] 在代码块匹配的括号之间跳转：<code>%</code></h3>
<p><code>%</code> 在匹配的括号之间跳转。需要将光标放在 <code>{}[]()</code> 上，然后按 <code>%</code>。 如果光标所在的位置不是 <code>{}[]()</code>，那么会向右查找第一个 <code>{}[]()</code>。</p>

<h3 id="f-光标跳转到前一个位置--后一个位置ctrl--o--ctrl--i">[F] 光标跳转到前一个位置 / 后一个位置：<code>&lt;Ctrl&gt; + o</code> / <code>&lt;Ctrl&gt; + i</code></h3>
<p>在标准模式下，<code>&lt;Ctrl&gt; + o</code> 将光标跳转到前一个位置，<code>&lt;Ctrl&gt; + i</code> 跳转到后一个位置。</p>

<p>注意这里使用的是“跳转”。<code>h</code> / <code>j</code>/ <code>k</code> / <code>l</code> / <code>w</code> 等移动将不会记录在「跳转表」中，只有通过 <code>gg</code> / <code>nG</code> / 查找时的 <code>n</code> / <code>N</code> 等命令执行的跳转操作，才可以通过 <code>&lt;Ctrl&gt; + o</code> / <code>&lt;Ctrl&gt; + i</code> 来回跳转。</p>

<blockquote>
  <p>补充：</p>
  <ul>
    <li>在 VS Code 中，向前一个 / 后一个位置跳转的快捷键是 <code>&lt;Ctrl&gt; + [</code> / <code>&lt;Ctrl&gt; + ]</code>。</li>
    <li>在 Intellij 等 Jetbrains 系列软件中，向前一个 / 后一个位置跳转的快捷键是 <code>&lt;Command&gt; + [</code> / <code>&lt;Command&gt; + ]</code>。如果不是，可以在 <code>Preferences</code> 中搜索 <code>back</code>，然后在 <code>KeyMap -&gt; Main menu -&gt; Navigate -&gt; Back</code> 中设置。</li>
  </ul>
</blockquote>

<h3 id="fe-替换文本rangesoldnewflag">[F][E] 替换文本：<code>:{range}s/{old}/{new}/{flag}</code></h3>
<p><code>:s</code>（substitute）命令用来查找和替换文本。语法如下：</p>

<pre><code>:{range}s/{old}/{new}/{flag}
</code></pre>

<p>表示在指定范围 <code>range</code> 内查找字符串 <code>old</code> 并替换为 <code>bar</code>，<code>flag</code> 说明了替换模式，如只替换首次出现、或全部替换。</p>

<h4 id="作用范围-range">作用范围 range</h4>
<p>作用范围分为当前行、全文、行范围、选区等：</p>
<ul>
  <li>当前行：空白，默认，如 <code>:s/foo/bar/g</code></li>
  <li>全文：<code>%</code>，如 <code>:%s/foo/bar/g</code></li>
  <li>n~m 行：<code>n,m</code>，如 <code>:5,12s/foo/bar/g</code> 表示 5~12 行</li>
  <li>当前行与之后 n 行：<code>.,+n</code>，如 <code>:.,+2s/foo/bar/g</code> 表示当前行与之后 2 行</li>
  <li>选区：略</li>
</ul>

<h4 id="替换模式-flag">替换模式 flag</h4>
<p>替换模式：</p>
<ul>
  <li>空白：默认，只替换光标位置之后的首次出现，如 <code>:%s/foo/bar</code></li>
  <li><code>g</code>：全局替换，替换每次出现（global），如 <code>:%s/foo/bar/g</code></li>
  <li><code>i</code>：忽略大小写</li>
  <li><code>c</code>：交互式替换，每次替换前需要用户确认（confirm），如 <code>:%s/foo/bar/gc</code> 表示查找全文的所有 <code>foo</code> 并替换为 <code>bar</code>，每次替换前都需要确认：
    <ul>
      <li>按下回车执行后，提示 <code>replace with bar (y/n/a/q/l/^E/^Y)?</code></li>
      <li><code>y</code> 表示替换</li>
      <li><code>n</code> 表示不替换</li>
      <li><code>a</code> 表示替换后续所有</li>
      <li><code>q</code> 表示退出查找模式</li>
      <li><code>l</code> 表示替换当前位置并退出查找模式</li>
      <li><code>^E</code>、<code>^Y</code> 用于向上、向下滚动屏幕，<code>^</code> 表示 <code>&lt;Ctrl&gt;</code> 键</li>
    </ul>
  </li>
</ul>

<h4 id="配合-v-可视模式选择替换区域">配合 <code>v</code> 可视模式选择替换区域</h4>

<p> 首先按 <code>v</code> 进入可视模式，选择要替换的文本范围。</p>

<p>然后按下<code>:</code> 进入命令行模式，Vim 将自动插入<code>:'&lt;,'&gt;</code>，表示选择的范围。</p>

<p>接下来，输入替换命令：</p>

<pre><code>:'&lt;,'&gt;s/old_text/new_text/g
</code></pre>

<p>这将在选择的区域内替换所有匹配到的 <code>old_text</code>。</p>

<h3 id="录制宏q">录制宏：<code>q</code>、<code>@</code></h3>

<p>Vim 的宏提供了将一系列操作记录下来然后重复执行的机制。它可以大大提高重复性的操作效率。使用宏的步骤如下：</p>

<ol>
  <li>录制宏。按下 <code>q</code> 键，然后再按下一个字母 (如 <code>a</code>，这是宏的名字)，开始录制宏。这时会在状态栏显示 <code>recording a</code>。在录制时，执行要重复的操作，包括移动、删除、插入等等。执行完操作后，按下 <code>q</code> 键结束录制。</li>
  <li>执行宏。按下 <code>@</code> 键，然后在输入框中输入之前记录宏的字母 (如 <code>a</code>)，按下回车键即可执行宏。也可以连续执行多次，比如执行 10 次，只需在 <code>@a</code> 后面加上 <code>10</code> 即可。</li>
</ol>

<p>另外，按下 <code>v</code> 键进入 visual mode 选中多行，可以批量针对多行文本执行宏。使用 <code>:reg</code> 命令可以查看所有已经保存的宏。如果在执行宏时出现错误，可以通过使用 <code>:debug</code> 命令进入调试模式。</p>

<h3 id="在-vim-中执行-shell-命令command">在 vim 中执行 shell 命令：<code>:!&lt;command&gt;</code></h3>

<p>比如通过 vim 编辑文本的时候，希望打印当前目录，但是又不想退出 vim，那么就可以直接在 vim 中执行：<code>:!pwd</code>，这等同于在 shell 中执行 <code>pwd</code>。</p>

<p>获得命令提示：</p>

<ul>
  <li>
    <p>在 vim 中输入 <code>:</code>，再按下 <code>&lt;Ctrl&gt; + d</code>，将展示所有可以在 vim 中使用的命令。</p>
  </li>
  <li>
    <p>输入 <code>:w</code>，再按下 <code>&lt;Ctrl&gt; + d</code>，将展示所有可以在 vim 中使用的、以 <code>w</code> 开头的命令。</p>
  </li>
</ul>

<h3 id="conf">配置文件</h3>
<p>配置文件位于 <code>~/.vimrc</code>，其内容是若干行可在 vim 中执行的命令，会在每次打开 vim 时自动执行。示例：</p>
<pre><code class="language-plaintext">set number # 显示行号
set releativenumber # 显示相对行号
set ignorecase # 大小写不敏感查找
set ic # 等价于 set ignorecase
set smartcase # 如果有一个大写字母，则切换到大小写敏感查找
set hls is # 高亮匹配项
</code></pre>

<p><a href="https://github.com/search?q=vimrc&amp;ref=opensearch">Github</a> 有很多开箱即用的 vimrc 文件，比如 <a href="https://github.com/amix/vimrc">amix/vimrc</a>。</p>

<p class="ant-alert ant-alert-info">💡 vim 可以修改键位映射。这里建议把高频使用的命令放在触手可及的键位上，比如 <code>^</code> (跳到当前行第一个非空字符) 就很常用，但这个键很难按。像 <code>W</code> / <code>E</code> / <code>R</code> / <code>S</code> 这些键位并不经常用，就可以配成其他操作。</p>

<h3 id="配置插件">配置插件</h3>

<ul>
  <li><a href="https://zhuanlan.zhihu.com/p/58816186">Vim 插件推荐</a></li>
  <li><a href="https://github.com/tpope/vim-surround">vim-surround</a> 是一个很好用的插件，功能和上面的 <a href="#inside-around">inside / around</a> 相反</li>
</ul>

<h2 id="其他工具-vim-化">其他工具 Vim 化</h2>

<h3 id="chrome">Chrome</h3>

<p><a href="https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb">Vimium</a>，通过类似 vim 风格的命令操作浏览器窗口。</p>

<h3 id="vs-code">VS Code</h3>

<p>Vim 插件，将 VS Code 的编辑器转为 vim 模式。最近高频使用，<code>V</code>、<code>ciw</code>、<code>di{</code> 等命令显著提升了编码效率。</p>

<p>编辑 <code>settings.json</code> 文件，配置 <code>jj</code> 替换 ESC：</p>

<pre><code>    "vim.insertModeKeyBindings": [
        {
            "before": ["j", "j"],
            "after": ["&lt;Esc&gt;"]
        }
    ]
</code></pre>

<h3 id="终端--bash--zsh">终端 / Bash / zsh</h3>

<p>在终端输入 <code>set -o vi</code> 可以切换到 vim 模式，按下 <code>ESC</code> 就能进入 vim 的 normal mode，修改终端命令的时候很好用。推荐将其写入 <code>.bashrc</code> 或 <code>.zshrc</code> 等配置文件。</p>

<p>在 zsh 中，也可以通过快捷键 <code>Ctrl + x, Ctrl + e</code> 打开 vim 编辑当前命令。</p>

<h2 id="总结">总结</h2>

<p>掌握「入门」一节中的快捷键，基本可以满足大部分使用场景。如果想进一步提升效率，那么「进阶」一节中的快捷键也值得学习。「高级」一节的内容，由于我还没有将 vim 作为主力开发工具，尚未深入研究，所以等以后有机会再补充。</p>

<p>可以在其他编辑器中配合 vim 插件，来培养 vim 的使用习惯。将 Chrome vim 化，也能体验到 vim 带来的酷炫与极客感。</p>

<p>最后，<strong>在实践中学习命令</strong>！如果只是阅读而不尝试，那么很快就会遗忘。</p>

<p>希望本文对你有帮助。</p>

<h2 id="附录-1速查表">附录 1：速查表</h2>
<p>仅作为正文的补充，记录一些可能有用的快捷键。</p>

<h3 id="光标移动">光标移动</h3>

<table>
  <thead>
    <tr>
      <th>快捷键</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>^</code></td>
      <td>移动到当前行第一个非空字符</td>
    </tr>
    <tr>
      <td><code>&lt;Space&gt;</code></td>
      <td>向右移动一个字符，等同于 <code>l</code></td>
    </tr>
    <tr>
      <td><code>&lt;Alt&gt; + ←</code>, <code>&lt;Alt&gt; + →</code></td>
      <td>向左 / 向右移动一个单词，等同于 <code>w</code> / <code>b</code></td>
    </tr>
    <tr>
      <td><code>:n&lt;enter&gt;</code></td>
      <td>跳到指定行，等同于 <code>nG</code></td>
    </tr>
    <tr>
      <td><code>ngg</code></td>
      <td>跳到指定行，等同于 <code>nG</code></td>
    </tr>
    <tr>
      <td><code>H</code></td>
      <td>光标移动到屏幕最上方（head）</td>
    </tr>
    <tr>
      <td><code>M</code></td>
      <td>光标移动到屏幕中央（middle）</td>
    </tr>
    <tr>
      <td><code>L</code></td>
      <td>光标移动到屏幕最下方（last）</td>
    </tr>
    <tr>
      <td><code>/{keyword}</code></td>
      <td>向后搜索，回车定位</td>
    </tr>
    <tr>
      <td><code>?{keyword}</code></td>
      <td>向前搜索，回车定位</td>
    </tr>
  </tbody>
</table>

<p class="ant-alert ant-alert-info"><strong>💡 Tips</strong>：如果想快速跳转到某个函数名，更建议<strong>用搜索</strong>。例如，如果你想将光标移动到某个函数的开头，可以使用 <strong><code>/{函数名}</code></strong> 命令来查找该函数名，然后再使用 <code>b</code> 命令将光标移动到函数名前的空格上。原因：如果目标位置距离较远，数需要跳转几行或几个单词很低效，而搜索可以直接跨行定位关键词。<strong><code>/</code></strong> 是向后搜索，<strong><code>?</code></strong> 是向前搜索。</p>

<h3 id="屏幕滚动">屏幕滚动</h3>

<table>
  <thead>
    <tr>
      <th>快捷键</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>向上 / 向下滚动一行</td>
      <td><code>&lt;Ctrl&gt; + y</code> / <code>&lt;Ctrl&gt; + e</code></td>
    </tr>
    <tr>
      <td>向上 / 向下滚动一页</td>
      <td><code>&lt;Ctrl&gt; + f</code> / <code>&lt;Ctrl&gt; + b</code>（forward，backward）</td>
    </tr>
    <tr>
      <td>向上 / 向下滚动半页</td>
      <td><code>&lt;Ctrl&gt; + d</code> / <code>&lt;Ctrl&gt; + u</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>这些命令在大部分 Unix 软件中都可以使用，比如 <code>man</code>、<code>less</code>、<code>tmux</code>（需要先进入滚动模式）</p>
</blockquote>

<h3 id="编辑">编辑</h3>

<table>
  <thead>
    <tr>
      <th>快捷键</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>J</code></td>
      <td>将当前行和下一行用空格连成一行</td>
    </tr>
    <tr>
      <td><code>Jx</code></td>
      <td>将当前行和下一行直接连成一行，相当于在下一行的行首按 <code>&lt;Backspace&gt;</code></td>
    </tr>
    <tr>
      <td><code>di(</code></td>
      <td>删除括号内的内容</td>
    </tr>
    <tr>
      <td><code>da(</code></td>
      <td>删除括号内的内容，包括括号本身</td>
    </tr>
    <tr>
      <td><code>ci(</code></td>
      <td>删除括号内的内容，同时进入编辑模式</td>
    </tr>
    <tr>
      <td><code>ddp</code></td>
      <td>上下两行交换，实际上就是 <code>dd</code> + <code>p</code></td>
    </tr>
  </tbody>
</table>

<h3 id="其他">其他</h3>

<table>
  <thead>
    <tr>
      <th>快捷键</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>:help</code></td>
      <td>查看帮助文档</td>
    </tr>
    <tr>
      <td><code>:help :{command}</code></td>
      <td>查看一个具体命令的帮助文档，如 <code>:help :q</code> 查看 <code>:q</code> 的帮助文档</td>
    </tr>
    <tr>
      <td><code>^y$</code></td>
      <td>复制一行</td>
    </tr>
    <tr>
      <td><code>ggyG</code></td>
      <td>复制整个文件</td>
    </tr>
    <tr>
      <td><code>q:</code></td>
      <td>查看历史命令，上下选择，按 <code>i</code> 编辑，回车执行，<code>:q</code>退出</td>
    </tr>
  </tbody>
</table>

<h2 id="附录-2vim-命令">附录 2：vim 命令</h2>
<p>可以在 vim 标准模式下输入 <code>:&lt;command&gt;</code> 执行，也可以写入配置文件。</p>

<pre><code>set number     # 显示行号

set ignorecase # 大小写不敏感查找
set smartcase  # 如果有一个大写字母，则切换到大小写敏感查找

imap ii &lt;Esc&gt;  # 在插入模式下，映射 ii 到 &lt;Esc&gt;

# 在标准模式下，禁用方向键
map &lt;Left&gt; &lt;Nop&gt;
map &lt;Right&gt; &lt;Nop&gt;
map &lt;Up&gt; &lt;Nop&gt;
map &lt;Down&gt; &lt;Nop&gt;

set paste # 进入粘贴模式，这可以避免粘贴多行代码时被自动缩进
set nopaste # 粘贴完之后，执行这条命令退出粘贴模式
</code></pre>

<p class="ant-alert ant-alert-info">📒  相关文章：<a href="/2023/gdb.html">💻【Linux】GDB 学习笔记</a></p>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[📺 视频分享：VS Code × Vim]]></summary></entry><entry><title type="html">⚙️【方法论】我的效率提升方法论 - 工具使用篇 ⑥</title><link href="https://imageslr.com/2022/efficiency-workflow.html" rel="alternate" type="text/html" title="⚙️【方法论】我的效率提升方法论 - 工具使用篇 ⑥" /><published>2022-05-01T23:00:00+08:00</published><updated>2023-03-14T00:00:00+08:00</updated><id>https://imageslr.com/2022/efficiency-workflow</id><content type="html" xml:base="https://imageslr.com/2022/efficiency-workflow.html"><![CDATA[<h2 id="〇前言">〇、前言</h2>

<p>现在市面上有太多的效率工具，我们很容易陷入一个误区：喜欢体验新鲜的工具，但没有明确的使用目的，仅仅是为了好玩，或者以为能提高生产力，到头来却发现是在浪费时间。因此在选择工具时，我参考了一些通用思维 (<a href="/2021/efficiency-01.html">📥 收件箱</a> <a href="/2021/efficiency-workspace.html">🔖 工作区</a> <a href="/2021/efficiency-01.html#advice">🪒 奥卡姆剃刀</a>)，先思考自己需要哪些功能，再去寻找提供这些功能的工具，在不同场景下构建了类似的工作流 (Workflow)。这样可以降低系统的复杂度，减轻工具带来的认知负担。</p>

<p>本文分享了我在日常工作场景中使用的一些效率工具，操作系统是 macOS。</p>

<blockquote>
  <p>macOS 的初始化可以参考 <a href="/2020/03/19/mac-initialization.html">💻 从零开始配置高效 Mac 开发环境</a>。</p>
</blockquote>

<h2 id="一浏览器">一、浏览器</h2>

<h3 id="标签页管理">标签页管理</h3>

<h4 id="场景">场景</h4>

<p>尽管我是一名程序员，但实际上我用浏览器的时间比写代码的时间还长，60% 以上的工作时间都是在 Chrome 浏览器中度过的：</p>

<p><img src="/media/image-20220227231120572.png" alt="image-20220227231120572" style="zoom:50%;" /></p>

<p class="caption">图：Chrome 的使用时间远超写代码的时间</p>

<p>浏览器已经快成为一个新的操作系统，无论是看文档、查资料、做表格、写周报，都离不开它。这导致我们常常会打开很多个标签页。据我观察，身边大多数同事的 Chrome 浏览器都是这样的：</p>

<p><img src="/media/image-20220213190712070.png" alt="image-20220213190712070" /></p>

<p>同时打开这么多的标签页，带来的问题也很明显：</p>

<ol>
  <li>浏览器占用了大量 CPU 和内存，导致系统运行卡顿。</li>
  <li>只能看到图标，看不到标题，无法快速找到特定的标签页。</li>
</ol>

<p>对于第一个问题，可以安装 <a href="https://chrome.google.com/webstore/detail/tab-suspender/fiabciakcmgepblmdkmemdbbkilneeeh">Tab Suspender</a> 插件解决，这个插件可以自动暂停长期未查看的标签页，节省内存。对于第二个问题，我安装过一些标签页搜索插件，Chrome 后来也提供了<a href="https://support.google.com/chrome/answer/10490587?hl=zh-Hans">内置的标签页搜索功能</a>，但这些方式都需要手动输入标签页的标题，很不方便。</p>

<p><img src="/media/image-20220308100927844.png" alt="image-20220308100927844" style="width:300px;" /></p>

<p class="caption">图：Chrome 内置的标签页搜索功能</p>

<p>我也尝试过使用 Chrome 自带的标签页分组功能，临时折叠一些标签页。但这个功能有点鸡肋：各个分组默认放在同一个窗口中，分组间的界限不明显，同时展开多个分组时，标签页还是被挤的只剩下个图标，而且 Chrome 关闭后分组信息也没了。</p>

<p><img src="/media/image-20220308100505029.png" alt="image-20220308100505029" /></p>

<p class="caption">图：Chrome 内置的标签页分组功能</p>

<h4 id="方案">方案</h4>

<p>我用 <a href="/2021/efficiency-workspace.html">🔖 工作区</a> 思维解决了「浏览器标签页管理」的问题。</p>

<p><strong>工作区思维的第一个要点：完成特定任务的场所、一系列关联资源的集合</strong>。我将 Chrome 分成多个窗口，每个窗口是一个“工作区”，包含和某项工作相关的全部标签页。通过将不同工作的上下文独立开来，可以减少混乱、提升注意力。</p>

<p>但是多窗口也带来一个问题：不同窗口间切换比较麻烦。Mac 系统的 <code>Command + Tab</code> 快捷键无法在相同应用间切换，<code>Command + `</code> 快捷键可以在同一个应用的不同窗口间切换，但没有预览界面。因此只能激活调度中心、肉眼判断每个窗口的内容是什么、然后选择一个窗口。</p>

<p><img src="/media/Xnip2022-02-27_23-39-51.jpg" alt="Xnip2022-02-27_23-39-51" /></p>

<p class="caption">图：Mac 触控板四指上划，打开调度中心，在多个 Chrome 窗口间切换</p>

<p>安装 <a href="https://alt-tab-macos.netlify.app/">AltTab</a> 插件可以完美地解决上述问题。这是一个即装即用的 Mac 窗口切换增强工具，按下 <code>Command + Tab</code> / <code>Command + `</code> 切换窗口时可以显示缩略图。详细配置方法见<a href="/2021/efficiency-workspace.html#alt-tab">这里</a>。</p>

<p><img src="/media/image-20220313233854266.png" alt="image-20220313233854266" /></p>

<p class="caption">图：安装 AltTab 之后，按下 <code>Command+`</code> 切换窗口时，会显示缩略图</p>

<p><strong>工作区思维的第二个要点：自动保存、用完即走、一键恢复</strong>。上面这套工作流的问题在于：Chrome 退出后，所有分组信息会全部消失。我习惯在周末关闭工作相关的窗口，周一再重新打开。有没有一个工具，能够自动保存我每个窗口的页面、在关闭后也能一键恢复？</p>

<p>经过一番搜寻，我找到了 <a href="https://chrome.google.com/webstore/detail/workspaces/hpljjefgmnkloakbfckghmlapghabgfa">Workspaces</a> 插件。尽管这是一个比较小众的插件，但是它和工作区思维完美契合：允许将多个标签页创建为一个工作区、<strong>自动保存</strong>当前工作区中打开的标签页、在重新打开工作区时<strong>自动恢复</strong>。</p>

<p>我将常用的场景、进行中的工作都保存成了工作区：</p>

<p><img src="/media/image-20220502000307963.png" alt="image-20220502000307963" style="zoom:50%;" /></p>

<p class="caption">图：我的工作区列表</p>

<p>有了这个插件，我不需要再同时打开很多个标签页或窗口，而是可以根据当前关注的事项，<strong>按需打开工作区</strong>。当我需要处理某项工作时，打开对应的工作区；处理完之后，直接关闭整个窗口。<strong>随用随开、用完即走</strong>，这极大限度地降低了干扰，减少了上下文切换的开销。</p>

<h4 id="总结">总结</h4>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p class="mb-1"><strong>推荐的工作流：</strong></p>
  <ol>
    <li>安装 <a href="https://chrome.google.com/webstore/detail/workspaces/hpljjefgmnkloakbfckghmlapghabgfa">Workspaces</a> 插件：
      <ul>
        <li>按照「场景」或「项目」划分工作区。</li>
        <li>随时关闭某个不使用的工作区窗口，用的时候再打开。</li>
        <li>激活 Workspace 插件的快捷键是 <code>Alt + w</code>。</li>
      </ul>
    </li>
    <li>安装 <a href="https://alt-tab-macos.netlify.app/">AltTab</a> 插件，通过快捷键 <code>Cmd + `</code> 快速切换窗口。</li>
    <li>安装 <a href="https://chrome.google.com/webstore/detail/tab-suspender/fiabciakcmgepblmdkmemdbbkilneeeh">TabSuspender</a> 插件，暂停长期不使用的标签页，节省内存。</li>
    <li>
      <details class="mb-0"><summary style="margin-bottom:0">安装 <a href="https://github.com/imageslr/chrome-tab-modifier">TabModifier</a> 插件，为标签页提供一个有辨识度的标题。</summary>下图左面两个标签页是不同的微服务，但使用了相同的标题；右面两个标签页则使用 TabModifier 插件修改了标题，更容易区分：<img src="/media/image-20220403233646549.png" /></details>
    </li>
    <li>
      <details class="mb-0"><summary style="margin-bottom:0">将窗口命名为工作区名称，切换或选择时会更具辨识度。</summary>
下面是默认的表现，每个窗口的名称是当前打开的标签页标题：<div>
<img width="45%" src="/media/image-20220404172500460.png" />
<img width="45%" src="/media/image-20220404172455221.png" /></div>
下面是修改窗口名称后的表现：<div>
<img width="45%" src="/media/image-20220404173002939.png" />
<img width="45%" src="/media/image-20220404173047815.png" /></div>
操作方式：在标签栏空白区域右键 - 为窗口命名，或者菜单栏 - 窗口 - 为窗口命名。</details>
    </li>
  </ol>

</div>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p class="mb-1"><strong>用 <a href="/2021/efficiency-01.html">📥 收件箱</a> 和 <a href="/2021/efficiency-workspace.html">🔖 工作区</a> 思维管理标签页和窗口：</strong></p>
  <ol>
    <li>打开一个 Inbox 窗口。Inbox 窗口是一个收件箱，放在这里的标签页全是<strong>待处理</strong>的，例如“待阅读”“待填写”“待评审”等。处理完后关闭。</li>
    <li>控制工作区窗口的数量。人的注意力是有限的，最多同时处理 2~3 件事。因此，最多同时打开三个工作区，当前不用的工作区通通关闭，减少分神。</li>
    <li>每个打开的标签页，都需要<strong>定期整理</strong>：要么移动到 Inbox 窗口，表示待处理；要么分配到特定的工作区窗口，持久保存；要么关闭。</li>
    <li>不需要每打开一个标签页就立刻整理。可以先进行手头的工作，等闲下来之后再整理。</li>
  </ol>

</div>

<div class="outline-box outline-box-hoverable mb-8 px-3 py-3">
  <p class="mb-1"><strong>如何在地址栏搜索标签页：</strong></p>

  <p>输入标题或 url 的内容，点击“切换到标签页”：</p>

  <p><img src="/media/image-20220407000918703.png" alt="image-20220407000918703" /></p>

</div>

<h3 id="书签管理">书签管理</h3>

<p>书签管理也是浏览器一个很重要的话题。我们会把任何可能有用的、或者感兴趣的网页存成书签，但往往是收藏的时候很顺手，想用的时候却找不到。下面是我解决这个问题的方法。</p>

<h4 id="1-为书签设置一个有意义的名称">(1) 为书签设置一个有意义的名称</h4>

<p><strong>Chrome 的地址栏支持搜索书签和历史记录</strong>。输入<strong>标题</strong>或 <strong>url</strong> 中的关键字，会加粗显示：</p>

<p><img src="/media/image-20220415000222673.png" alt="image-20220415000222673" /></p>

<p class="caption">图：在地址栏搜索关键字，会在标题和 url 中加粗显示</p>

<p>因此，可以为每个书签设置一个有意义的名称。当需要查找一个书签时，直接在地址栏输入几个关键字，比先思考它属于哪个类别、再去查找对应的文件夹要更方便。</p>

<p>我采用<code>[平台]名称</code> 的命名方式，比如 <code>[Gitlab]imageslr/blog</code>、<code>[TCC]ad.engine.api</code>。这里可以配合 <a href="https://github.com/imageslr/chrome-tab-modifier">TabModifier</a> 插件，使标签页的标题和书签名称一致。</p>

<p>书签名还可以添加一些辅助搜索的 <strong>SEO 短语</strong>，比如 <code>性能平台-云服务</code> 可以修改为 <code>性能平台-云服务|golang pprof|profile|服务性能优化|内存泄露排查</code>：</p>

<p><img src="/media/image-20220410234734913.png" alt="image-20220410234734913" /></p>

<p class="caption">图：添加一些描述页面功能的、合乎直觉的、在搜索时很容易能回想起来的短语</p>

<p>当书签名足够有信息量时，我们甚至不需要书签栏，直接在地址栏搜索关键字就能打开想要的书签。事实上，我在使用浏览器时，书签栏就始终是隐藏状态。</p>

<p class="ant-alert ant-alert-info">💡 这里再推荐一个 Chrome 插件：<a href="https://chrome.google.com/webstore/detail/holmes/gokficnebmomagijbakglkcmhdbchbhn">Holmes</a>。安装后，在地址栏输入 <code>*</code> 再按 <code>Tab</code>，就能搜索书签了。</p>

<h4 id="2-使用文件夹管理书签">(2) 使用文件夹管理书签</h4>

<p>不要把书签直接保存在书签栏上，而是要放在文件夹里。书签的标题会占用书签栏的空间。</p>

<p>不需要创建层层嵌套的文件夹。一般来说，在书签命名良好的情况下，我们可以很快搜索到想要的内容。因此，书签的文件夹只需要简单的划分，粒度可以粗一些，层级可以扁平一些。<a href="#bookmark">附录</a>是我的书签分类方式。</p>

<h4 id="3-将参考资料移动到别处">(3) 将参考资料移动到别处</h4>

<p>许多书签实际上是”参考资料“ —— 或者是对某项工作有用的参考文档，或者是一些学习资料，又或者是一些感兴趣的文章。我们需要定期整理书签栏，将这些”参考资料“移动到别处：</p>

<ul>
  <li>某项工作的参考文档：移动到这项工作的项目文档。</li>
  <li>学习资料：移动到学习笔记，或者移动到任务清单，作为一个 TODO。</li>
  <li>感兴趣的文章：移动到稍后读工具。</li>
</ul>

<p>总之，”参考资料“应当移动到特定的<em>上下文</em> 中，而不是放在书签栏里石沉大海。书签栏只保留那些需要经常打开的、真正有用的页面，减少干扰，易于维护。</p>

<h2 id="二alfred">二、Alfred</h2>

<p>Spotlight 是 Mac 系统内置的一个快速搜索工具。市面上有一些类 Spotlight 工具，提供了不输于原生 Spotlight 的搜索功能、丰富的效率工具、以及高度的自定义能力。最常见的是 <a href="https://www.alfredapp.com/">Alfred</a>、<a href="https://u.tools/">uTools</a>、<a href="https://www.raycast.com/">Raycast</a>，网上有很多介绍这三个工具的文章，此处不再赘述。</p>

<p>个人认为，这类工具提升效率的关键在于：<strong>(1) 多用键盘，少用鼠标；(2) Don’t Repeat Yourself</strong>，通过自定义配置，减少重复操作。下面会举例说明。</p>

<p>我使用的是 Alfred。它的功能很全，插件丰富，就算不折腾，默认功能也已经足够好用。下面罗列了一些我常用的功能。</p>

<p class="ant-alert ant-alert-info">💡 在 2023 年的今天，我更推荐使用 <a href="https://www.raycast.com/">Raycast</a>。它涵盖了 Alfred 的几乎所有功能，但界面更美观、更易用。<a href="#raycast">附录</a>是 Raycast 和 Alfred 的对比。</p>

<h3 id="基本设置">基本设置</h3>

<ul>
  <li>快捷键：双击 <code>Alt</code>。这样一只手就能激活 Alfred。</li>
  <li>关闭 <code>Shift</code> 预览：Preferences - Features - Previews - Quick Look (取消勾选)。这个预览功能其实没啥用，还很容易误触。</li>
</ul>

<h3 id="打开--切换-app">打开 / 切换 App</h3>

<div class="ant-alert ant-alert-info">
  <p>👎   鼠标移动到 Dock 栏，点击图标。<br />
👍   激活 Alfred，输入 App 拼音的前几个字母，回车。</p>
</div>

<p>操作鼠标是一个很低效的动作。每次都需要右手先离开键盘、找到鼠标、移动和点击、再把手放回键盘，重新校准手指位置；这个过程中，眼睛还必须配合鼠标指针的移动。</p>

<p>建议使用 Alfred 充当 App 启动器。输入 App 名称 (拼音或首字母) 即可启动，双手不需要离开键盘，速度更快、更方便。</p>

<p><img src="/media/image-20220413231612708.png" alt="image-20220413231612708" style="zoom:33%;" /></p>

<p class="caption">图：使用 Alfred 查找 App，按 <code>Cmd + n</code> 打开</p>

<p>此外，还可以为常用的 App 配置<strong>全局快捷键</strong>，便捷切换可见状态。配置方法见 Preferences - Workflows - 右下角加号 - Getting Started - Hotkeys。比如我把 <code>Alt+Q</code>、<code>Alt+E</code>、<code>Alt+F</code> 分配给了提醒事项、飞书和微信。</p>

<p><img src="/media/image-20220413230408023.png" alt="image-20220413230408023" style="zoom: 25%;" /></p>

<p class="caption">图：配置一个简单的 workflow，就可以通过快捷键显示 / 隐藏 App</p>

<h3 id="剪贴板历史--snippets">剪贴板历史 / Snippets</h3>

<div class="ant-alert ant-alert-info">
  <p>👎   使用两个工具，分别管理剪贴板历史和代码片段，资源占用大、操作流程长。<br />
👍   使用 Alfred 解决所有问题。</p>
</div>

<p>Alfred 内置了剪贴板历史工具，非常好用，且资源占用小。我用它替换了 iPaste。配置方式：</p>

<ul>
  <li>Preference - Features - Clipboard History，我的快捷键是 <code>Cmd + Shift + V</code></li>
  <li>勾选上 <code>Keep Plain Text</code>、<code>Keep Images</code> 和 <code>Keep File List</code> 以同时保存文本和文件</li>
  <li>在“Advanced”里勾选 <code>Auto-paste on return</code>，这样按下回车后就会自动粘贴</li>
</ul>

<p>这之后就可以通过快捷键查看历史记录了。上下键选择某条记录，回车粘贴，也可以通过 <strong><code>Cmd + 数字</code></strong> 直接选择；支持输入关键字搜索。</p>

<p><img src="/media/clipboard-viewer.png" alt="Clipboard History" /></p>

<p>Alfred 剪贴板工具的最赞之处在于能够<strong>和 Snippets 联动</strong>。</p>

<p>Alfred 内置了一个 Snippets 管理工具，可以创建多个清单来管理自己的代码片段：</p>

<p><img src="/media/snippets-prefs.png" alt="Snippets Prefs" /></p>

<p>我把经常执行的一些命令保存成了代码片段，这之后就可以直接在剪贴板历史里查看了，也可以根据关键字搜索代码段的名称或内容：</p>

<p><img src="/media/snippet-viewer-this-land.png" alt="Snippets Viewer" /></p>

<p><strong>剪贴板历史的内容，可以直接保存到 Snippets 里</strong>。只需要呼出剪贴板历史工具，选中一行，然后按 <code>Cmd + S</code> 快捷键。强烈推荐使用这个功能，大幅降低录入成本。</p>

<p>Snippets 的详细使用说明见 <a href="https://www.alfredapp.com/help/features/snippets/">Alfred 官网</a>。</p>

<h3 id="搜索文件">搜索文件</h3>

<div class="ant-alert ant-alert-info">
  <p>👎   在 Finder 中手动查看每个文件夹，或者使用 Finder 的搜索功能。<br />
👍   激活 Alfred，输入文件名，回车。</p>
</div>

<p>Alfred 的搜索功能很强大。只需记住这两个命令：</p>
<ul>
  <li><code>空格 + 文件名</code>：按文件名搜索，支持拼音。</li>
  <li><code>in + 字符串</code>：按文件内容搜索。</li>
</ul>

<p><img src="/media/image-20220413232520845.png" alt="image-20220413232520845" style="zoom:33%;" /></p>

<p class="caption">图：空格 + 文件名，搜索文件</p>

<p><img src="/media/image-20220413232728058.png" alt="image-20220413232728058" style="zoom:33%;" /></p>

<p class="caption">图：in + 文件名，搜索文件内容</p>

<p>选中搜索结果后，按 <code>Enter</code> 打开文件，按 <code>Command + Enter</code> 打开文件所在的文件夹。</p>

<p>此外，还可以自定义<b>搜索过滤器</b>，获得更精确的搜索结果。比如我经常会搜索自己的笔记，格式都是 markdown，于是便配置了一个只搜索 <code>.md</code> 文件的 workflow。配置方法见<a href="#file-filter">附录</a>。</p>

<p><img src="/media/image-20220414000949319.png" alt="image-20220414000949319" style="zoom:33%;" /></p>

<p class="caption">图：自定义 Workflow，只搜索 markdown 文件，既能搜索文件名，也能搜索文件内容</p>

<h3 id="web-search">Web Search</h3>

<div class="ant-alert ant-alert-info">
  <p>👎   打开浏览器 - 进入搜索页 - 点击搜索栏 - 输入搜索内容 - 回车<br />
👍   激活 Alfred - 输入搜索内容 - 回车</p>
</div>

<p>Web Search 是 Alfred 的一大特色功能。在 Alfred 输入要搜索的内容、回车，就可以立刻打开搜索结果页。</p>

<p>Alfred 内置了很多搜索引擎 (Preferences - Features - Web Search)：</p>

<p><img src="/media/web-search-prefs.png" alt="Web Searches" /></p>

<p>使用时，需要输入搜索引擎的 <code>Keyword</code>，然后在空格后输入要搜索的内容，例如 <code>google something</code>、<code>gmail something</code>。</p>

<p><strong>可以将常用的搜索引擎设置为默认结果</strong>，这样就不需要输入关键字了。配置路径在 Preferences - Features - Default Results - Fallbacks - Setup fallback results：</p>

<p><img src="/media/image-20220417003831536.png" alt="image-20220417003831536" /></p>

<p class="caption">图：新增默认搜索结果</p>

<p>我设置的 fallback results 是 <strong>Google</strong> 和<strong>公司内网搜索</strong>：</p>

<p><img src="/media/image-20220417001602114.png" alt="image-20220417001602114" style="zoom:33%;" /></p>

<p class="caption">图：输入任何内容，都可以在 Google 或内网搜索，回车或 Command+2 打开结果页</p>

<p><strong>Alfred 还可以自定义 Web Search</strong>。我们每天除了使用 Google 等搜索引擎，还会在公司的许多内部平台搜索，比如搜索代码库、搜索机器 IP、搜索内网等。这些平台的搜索功能都可以配置为自定义 Web Search，从而省去和浏览器的交互。配置方法见<a href="#web-search-config">附录</a>。</p>

<p><strong>格式固定的 url 也可以配置为 Web Search</strong>。比如：</p>

<ul>
  <li>github 的链接格式是 <code>github.com/用户名/仓库名</code>，我配置了一个 Web Search：<code>https://github.com/{query}</code>。之后在 Alfred 中输入 <code>github vuejs/vue</code>，就可以直接打开 <a href="https://github.com/vuejs/vue">https://github.com/vuejs/vue</a>。</li>
  <li>公司内部服务平台的链接格式是 <code>https://cloud.xxx.net/service/服务名</code>，我也配置成了 Web Search：<code>https://cloud.xxx.net/service/{query}</code>，这样连搜索的步骤都省下了。</li>
</ul>

<p>最后，<strong>尽量通过 Alfred 执行搜索操作</strong>。只需专注于内容本身，完全不需要任何浏览器操作。</p>

<p class="ant-alert">💡   字节跳动的同学可以在内网搜索“Alfred Web Search 合集”，获取我整理的十余个内部 Web Search 配置。</p>

<h3 id="workflow">Workflow</h3>

<p>Workflow 是 Alfred 的核心功能。Workflow 类似于 iOS / Mac 的「快捷指令」，通过可视化的方式串联一系列操作，之后用一个命令直接执行整个流程。很多工作中的重复性操作都可以配置为 Workflow，节省时间，提高效率。</p>

<p>网络上有许多 Alfred Workflow 资源：</p>

<ul>
  <li><a href="https://www.alfredapp.com/workflows/">https://www.alfredapp.com/workflows/</a></li>
  <li><a href="https://github.com/alfred-workflows/awesome-alfred-workflows">https://github.com/alfred-workflows/awesome-alfred-workflows</a></li>
</ul>

<p>我常用的是这几个：</p>

<ul>
  <li><a href="https://github.com/wensonsmith/YoudaoTranslator">有道翻译</a></li>
  <li><a href="https://bytedance.feishu.cn/docs/doccnk9WQ7yWlOLcpAbT072A5ve">Lark 云文档搜索</a></li>
  <li><a href="https://github.com/tianhao/alfred-mweb-workflow">MWeb 文档搜索</a></li>
</ul>

<p><strong>Don’t Repeat Yourself</strong>。多观察自己有哪些重复的操作，尝试把它配置成 workflow。举个例子，我经常需要执行一个命令，里面包含了 <code>1.0.1</code> 这样的版本号，版本号每次执行都不一样。一开始，我是手动填充版本号。后来配置了一个 Workflow，只需要输入版本号，就能自动拼接完整命令，并复制到剪贴板，非常方便。</p>

<p><img src="/media/image-20220417161324541.png" alt="image-20220417161324541" /></p>

<p class="caption">图：输入 curl_code，再输入版本号，就能将命令复制到剪贴板，可以直接去粘贴运行</p>

<p>Alfred Workflow 的配置教程可以在 <a href="https://www.alfredapp.com/help/workflows/">Alfred 官网</a> 查看。Alfred 中也内置了许多示例教程，见 Preferences - Workflows - 左下角加号 + 。</p>

<h2 id="三个人知识库">三、个人知识库</h2>

<p>每个人都需要一个知识库。知识库最大的意义是充当大脑外存，帮助我们管理知识，并在需要的时候快速查阅。一方面，我们学习的新知识，如果不经常使用，很快就会忘记，因此需要整理在知识库里，以便日后复习。另一方面，我们总是会遇到各种问题，每次都去 Google 无疑会浪费时间，如果记录在知识库里，下次就可以直接在知识库检索，事半功倍。</p>

<p>在大一时，我就开始有意识地搭建个人知识库，至今已经积累了 1200 多篇笔记。尽管这些笔记里有很多都是偶尔才会打开，但因为都是用自己熟悉的方式记录的，所以往往扫一眼就能回想起完整的上下文，节省了从许多原始资料中筛选重点内容的时间。</p>

<p>我的知识库管理应用是 <a href="/2020/03/19/mac-initialization.html#mweb">MWeb</a>。下面是我的一些使用心得。</p>

<h3 id="markdown">All In Markdown</h3>

<p>Markdown 是一种用来写作的轻量级标记式语言，它使用简洁的纯文本格式来编写文档，可以转换成有效的 HTML 或 PDF 文档。Markdown 最重要的设计是易读易写 —— 语法轻量化；纯文本格式也能够直接在字面上被阅读。</p>

<p>Markdown 不需要像 Word 那样先选中文字、再点击工具栏的图标，常见的排版都可以用键盘完成。使用 Markdown 写作，我们可以专注于内容本身，更流畅地表达自己的思路。</p>

<p>每个程序员都应该学习 Markdown、使用 Markdown。Markdown 的语法十分简单，常用的标记符号不超过十个，几分钟就能掌握。目前许多网站都支持 Markdown 语法，如 Github、少数派、石墨文档、飞书文档等。我的博客也是用 Markdown 写的。</p>

<p>我会优先选择支持 Markdown 完整语法的笔记应用。目前，我使用 <a href="/2020/03/19/mac-initialization.html#mweb">MWeb</a> 管理自己的所有笔记；当需要输出长文时，我会配合使用 <a href="https://typoraio.cn/">Typora</a>。</p>

<blockquote>
  <p><a href="https://sspai.com/post/25137">少数派：认识与入门 Markdown</a></p>
</blockquote>

<h3 id="随手记">随手记</h3>

<p><strong>知识的输入</strong>是构建个人知识库的重要一环。我们经常会在各种场景下遇到<strong>碎片化</strong>的信息：或者是与同事交流时，了解到一个业务背景；或者是看某篇文档时，发现一个名词解释；或者是查一个问题时，学到一个新的工具…… 这些知识都是有用的，但我们很少有时间可以停下手头的工作，去整理这些内容。这时，一个<strong>触手可得</strong>的随手记工具就显得尤为重要。</p>

<p>随手记工具是<strong>知识的缓冲区、收件箱</strong>。任何时候，只要遇到有用的知识，就随手记录下来。每隔一段时间，再把随手记的内容整理到个人知识库中。将知识管理分为「收集」和「整理」两步，可以简化知识录入的成本，在不打断当前工作心流的前提下，捕捉每个重要信息。</p>

<p>随手记工具的核心在于<strong>快速</strong>。我使用的是 MWeb 的 <a href="https://zh.mweb.im/15303794142935.html">快速笔记</a> 功能，按下快捷键，就可以记录 Markdown 内容。随手记工具也可以是和知识库分开的，比如你也可以使用 Mac 的 <a href="https://support.apple.com/zh-cn/guide/notes/apdf028f7034/4.9/mac/12.0">快速备忘录</a>，或者 Drafts 等任何趁手的工具。重点在于<strong>定期整理</strong>、定期清空随手记中的内容。</p>

<p class="ant-alert">💡   进一步阅读：<a href="/2021/efficiency-01.html#information">📥 收件箱思维 - 信息管理</a></p>

<h3 id="可搜索性">可搜索性</h3>

<p>个人知识库的可搜索性很重要。如果每次查阅时都很不方便，那么知识库就失去了作为大脑外存的意义。可以从以下几点来提升知识库的可搜索性：(1) 结构；(2) 标题和内容；(3) SEO 关键词。</p>

<h4 id="结构">结构</h4>

<p>当我们在图书馆查找一本书时，可以根据图书分类法，很快定位到一本图书。同理，为知识库设置合理的文件夹层级，也可以帮助我们快速定位一篇笔记。</p>

<p>知识库的结构没有统一的规范，符合个人认知即可。下面是一个示例：</p>

<ul>
  <li>工作记录
    <ul>
      <li>XX 项目开发</li>
      <li>XX 工具调研</li>
    </ul>
  </li>
  <li>源码分析</li>
  <li>运维手册</li>
  <li>学习笔记
    <ul>
      <li>C++</li>
      <li>Golang</li>
    </ul>
  </li>
  <li>个人
    <ul>
      <li>面试求职</li>
      <li>双月计划</li>
    </ul>
  </li>
  <li>…</li>
</ul>

<p>这里我的建议是：<strong>如非必要，勿增实体</strong>。不要在一开始就设置非常详细的层级结构，这样只会加重选择困难。前期最好只设置必要的文件夹，层级尽量扁平，比如「工作」「个人」「学习」等。之后当笔记的数量积累到一定程度时，再拆分成更细粒度的分组。总之，渐进式地迭代我们的知识库系统，而不是追求一步到位。</p>

<h4 id="标题和内容">标题和内容</h4>

<ol>
  <li>标题要有信息量，方便在搜索结果中定位。标题中可以附带一些关键词。</li>
  <li>合并内容重复的、相似的笔记，减少搜索结果中的干扰项。</li>
</ol>

<h4 id="seo-关键词">SEO 关键词</h4>

<p>当我们搜索一篇笔记时，往往想到的都是一些离散的关键词，而不是一句连续的话。因此，可以在正文中添加一些辅助搜索的 <a href="https://zh.wikipedia.org/wiki/%E6%90%9C%E5%B0%8B%E5%BC%95%E6%93%8E%E6%9C%80%E4%BD%B3%E5%8C%96">SEO</a> 关键词。思考一下，当你看到这篇笔记时，最先想到的是哪些词语，这些词语就可以作为它的关键词。</p>

<p>关键词的格式要特殊一些，以便和正文内容区分，比如我设置的是 <code>[XXX]</code>：</p>

<p><img src="/media/image-20220501211734069.png" alt="image-20220501211734069" style="zoom:50%;" /></p>

<p class="caption">图：笔记示例，上面的 <code>[文件描述符]</code> <code>[stderr]</code> 等就是 SEO 关键词</p>

<p>搜索时，可以组合搜索关键词和正文内容：</p>

<p><img src="/media/image-20220501212415500.png" alt="image-20220501212415500" style="zoom:33%;" /></p>

<p class="caption">图：使用 <a href="https://github.com/tianhao/alfred-mweb-workflow">mweb alfred workflow</a> 搜索 MWeb 中的笔记</p>

<h3 id="知识库应用">知识库应用</h3>

<p>最后，我们讨论应该如何选择一款知识库应用。</p>

<p>我的知识库应用是 <a href="/2020/03/19/mac-initialization.html#mweb">MWeb</a>。对我来说，它的优点是：</p>

<ul>
  <li>支持完整的 Markdown 语法，包括 LaTex 公式、流程图等 <em class="text-gray-400">(语法完整)</em></li>
  <li>可以直接在编辑器中粘贴图片，会自动转换为 Markdown 语法 <em class="text-gray-400">(写作流畅)</em></li>
  <li>无限层级的文件夹；文档支持自定义排序 <em class="text-gray-400">(文档管理能力)</em></li>
  <li>开发式文档库，可以使用其他工具编辑、搜索  <code>.md</code> 文件 <em class="text-gray-400">(可扩展性)</em></li>
  <li>内置的「快速笔记」功能 <em class="text-gray-400">(输入 → 整理 → 输出一条龙)</em></li>
  <li>Mac 原生应用，也有 iOS App <em class="text-gray-400">(比网页响应速度更快)</em></li>
  <li>支持自定义主题</li>
</ul>

<p>缺点是：</p>

<ul>
  <li>不支持所见即所得 <em class="text-gray-400">(但可以使用 Typora 写作、MWeb 专注于文档管理)</em></li>
  <li>搜索能力一般，不支持正则表达式 <em class="text-gray-400">(可以自己开发一个 alfred workflow，但成本较高)</em></li>
  <li>因为文档在 Mac 本地存储，使用 iCloud 同步，所以 iOS 加载文档的速度较慢 <em class="text-gray-400">(但我几乎不使用 iOS 查看笔记)</em></li>
  <li>不支持双向链接</li>
</ul>

<p>类似的知识库应用还有<a href="https://b3log.org/siyuan/download.html">思源笔记</a>， 支持本地文档库、双向链接、所见即所得。此外还有 Web 版的知识库应用，如<a href="https://www.feishu.cn/product/docs">飞书云文档</a>、<a href="https://www.notion.so/zh-cn">Notion</a> 等。这些应用都支持 Markdown 语法，功能上各有优劣，请按实际需求选用。如果读者有推荐的知识库应用，也欢迎评论区补充。</p>

<h2 id="四理念">四、理念</h2>

<h3 id="如非必要勿增实体">如非必要，勿增实体</h3>

<ul>
  <li>使用一组简单的工具，完成不同的需求；而不是使用一个复杂的工具，完成全部的需求。</li>
  <li>使用一个复杂的工具时，前期只使用必要的功能。不要强行迎合软件，而是要根据实际需求，渐进式地选用新功能。</li>
</ul>

<h3 id="dont-repeat-yourself">Don’t Repeat Yourself</h3>

<ul>
  <li>经常需要复制粘贴的内容 (如地址、邮箱、发票抬头、工号等)，存成 Snippet。电脑上可以使用 <a href="https://sspai.com/post/46034">Alfred Snippet</a>；手机上可以使用备忘录，或者在输入法中配置快捷短语。</li>
  <li>重复性操作，配置成 <a href="https://support.apple.com/zh-cn/guide/shortcuts-mac/apd163eb9f95/mac">快捷指令</a> 或 <a href="#workflow">Alfred Workflow</a>。</li>
</ul>

<h3 id="善用-seo-关键词">善用 SEO 关键词</h3>

<p>SEO 关键词可以增加信息量，提升检索效率。下面这些位置可以添加 SEO 关键词：</p>

<ul>
  <li>Chrome 书签标题</li>
  <li>笔记标题、笔记内容</li>
  <li>文件名</li>
  <li><em>… (所有要查找的位置)</em></li>
</ul>

<h3 id="多用键盘少用鼠标">多用键盘，少用鼠标</h3>

<ol>
  <li>使用 Alfred 作为 App 启动器；使用 Alfred 搜索文件；配置 <a href="#web-search">Alfred Web Search</a>。</li>
  <li><strong>学会这几个文本操作快捷键</strong>，适用于任何文本编辑的场景：
    <ul>
      <li>移动光标：
        <ul>
          <li>移动一个字符：<code>←</code>、<code>→</code></li>
          <li>移动一个单词：<code>Alt + ←</code>、<code>Alt + →</code></li>
          <li>移动到行首 / 行尾：<code>⌘Cmd + ←</code>、<code>Cmd + →</code>，或者 <code>Ctrl + a</code>、<code>Ctrl + e</code></li>
        </ul>
      </li>
      <li>选中文本：
        <ul>
          <li>选中一个字符：<code>Shift + ←</code>、<code>Shift + →</code></li>
          <li>选中一个单词：<code>Shift + Alt + ←</code>、<code>Shift + Alt + →</code></li>
          <li>选中到行首 / 行尾：<code>Shift + Cmd + ←</code>、<code>Shift + Cmd + →</code></li>
        </ul>
      </li>
      <li>删除文本：
        <ul>
          <li>删除一个字符：<code>← Backspace</code></li>
          <li>删除一个单词：<code>Alt + ← Backspace</code></li>
          <li>删除到行首：<code>Cmd +  ← Backspace</code></li>
        </ul>
      </li>
      <li>你会发现其中的一些模式：<code>Alt</code> 操作单词、<code>Cmd</code> 操作整行、<code>Shift</code> 选中文本。</li>
    </ul>
  </li>
  <li>一些有用的快捷键：
    <ul>
      <li><code>Cmd + A</code> (全选)、 <code>Cmd + Z</code> (撤销)、<code>Cmd + Shift + Z</code> (重做)</li>
      <li>文本编辑器： <code>Cmd + B</code> (加粗)、<code>Cmd + I</code> (斜体)、<code>Cmd + U</code> (下划线)</li>
      <li>Chrome 左 / 右切换标签页：<code>Cmd + Shift + [</code> 、<code>Cmd + Shift + ]</code></li>
      <li>Mac 切换应用窗口 (建议配合 <a href="https://alt-tab-macos.netlify.app/">AltTab</a> 使用)：</li>
    </ul>
    <ul>
      <li>不同应用程序：<code>Cmd + Tab</code>、<code>Cmd + Shift + Tab</code></li>
      <li>相同应用程序：<code>Cmd + `</code></li>
    </ul>
  </li>
  <li><strong>不同的软件快捷键可以配置成一样的，便于记忆</strong>。比如：
    <ul>
      <li>我经常使用飞书云文档、MWeb 和 Typora 编写 Markdown 文件。因为飞书云文档不支持自定义快捷键，所以我把其他两个软件的 Markdown 编辑快捷键都配置成和飞书云文档一样的。</li>
      <li>Chrome、iTerm2 都可以按 <code>Cmd + n</code> (n=1,2,3…) 来切换标签页，我给 VS Code 也配置了同样的快捷键，按 <code>Cmd + n</code> 可以切换当前打开的源文件。</li>
    </ul>
  </li>
</ol>

<p class="ant-alert ant-alert-warning">💡   Mac 的某些应用程序没有提供快捷键配置入口，这种情况下可以在系统偏好设置中更改，详见<a href="#shortcut-config">附录</a>。</p>

<h2 id="五我日常使用的工具列表">五、我日常使用的工具列表</h2>

<blockquote>
  <p><a href="/2020/03/19/mac-initialization.html">💻 从零开始配置高效 Mac 开发环境</a></p>
</blockquote>

<ul>
  <li>写作：<a href="https://typoraio.cn/">Typora</a></li>
  <li>知识库：<a href="/2020/03/19/mac-initialization.html#mweb">MWeb</a></li>
  <li>截图：<a href="https://zh.xnipapp.com/">Xnip</a></li>
  <li>Mac 应用切换增强插件：<a href="https://alt-tab-macos.netlify.app/">AltTab</a>，切换应用时显示缩略图</li>
  <li>Mac 窗口布局工具：<a href="https://rectangleapp.com/">Rectangle</a></li>
  <li>划词翻译：<a href="https://github.com/ripperhe/Bob">Bob</a></li>
  <li>稍后阅读：<a href="https://cubox.pro/">Cubox</a></li>
  <li>待办事项管理：<a href="https://www.omnigroup.com/omnifocus/">OmniFocus</a> <em class="text-gray-400">(个人)</em>、<a href="https://dida365.com/">滴答清单</a> <em class="text-gray-400">(工作)</em></li>
  <li>番茄钟：<a href="https://hovancik.net/stretchly/downloads/">Stretchly</a></li>
  <li>Alfred 常用 Workflow：
    <ul>
      <li><a href="https://github.com/wensonsmith/YoudaoTranslator">有道翻译</a></li>
      <li><a href="https://bytedance.feishu.cn/docs/doccnk9WQ7yWlOLcpAbT072A5ve">Lark 云文档搜索</a></li>
      <li><a href="https://github.com/tianhao/alfred-mweb-workflow">MWeb 文档搜索</a></li>
    </ul>
  </li>
  <li>VS Code 常用插件：
    <ul>
      <li><a href="https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager">Project Manager</a>：工作区管理</li>
      <li><a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens">GitLens</a></li>
    </ul>
  </li>
  <li>Chrome 常用插件：
    <ul>
      <li><a href="https://chrome.google.com/webstore/detail/workspaces/hpljjefgmnkloakbfckghmlapghabgfa">Workspaces</a>：工作区管理</li>
      <li><a href="https://chrome.google.com/webstore/detail/tab-suspender/fiabciakcmgepblmdkmemdbbkilneeeh">Tab Suspender</a>：自动暂停长期不活动的选项卡，节省内存</li>
      <li><a href="https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb">Vimium</a>：使用 vim 快捷键操作网页</li>
      <li><a href="https://chrome.google.com/webstore/detail/open-tabs-next-to-current/gmpnnmonpnnmnhpdldahlekfofigiffh?hl=zh-CN">Open Tabs Next to Current</a>：按下 <code>Cmd + T</code> 时，在当前标签页的右边新建标签页</li>
      <li><a href="https://chrome.google.com/webstore/detail/gofullpage-full-page-scre/fdpohaocaechififmbbbbbknoalclacl">GoFullPage</a>：网页滚动截图</li>
    </ul>
  </li>
</ul>

<h2 id="六附录">六、附录</h2>

<h3 id="bookmark">书签文件夹示例</h3>

<ul>
  <li>快速入口：经常浏览的网站，比如工作日报、Github Trending、文档库首页等。</li>
  <li>Inbox：待整理的内容。</li>
  <li>Workspace：里面是一些子文件夹，每个子文件夹是一项具体的工作。</li>
  <li>📒：常用的参考手册、知识库链接。</li>
  <li>☁️：公司内部的云平台链接，比如代码库、微服务、动态配置中心等。</li>
  <li>Metrics：常用的 metrics 打点。</li>
  <li>Monitor：各种监控大盘。</li>
  <li>Tool：常用的工具，比如 JSON 格式化、DAG 可视化、正则表达式测试等。</li>
  <li>Archive：归档。</li>
</ul>

<p><img src="/media/image-20220404211219820.png" alt="image-20220404211219820" /></p>

<h3 id="file-filter">Alfred 搜索过滤器配置方法</h3>

<ol>
  <li>根据模板创建 Workflow：Preferences - Workflows - 右下角加号 - Examples - Simple File Search。
 <img src="/media/image-20220414000257947.png" alt="image-20220414000257947" /></li>
  <li>双击修改 File Filter。</li>
  <li>只搜索 <code>.md</code> 文件。这里可以把一个 <code>.md</code> 文件拖进去，会自动设置文件类型：
 <img src="/media/image-20220413235656912.png" alt="image-20220413235656912" /></li>
  <li>只搜索指定目录：
 <img src="/media/image-20220414000450840.png" alt="image-20220414000450840" /></li>
  <li>不仅搜索文件名，也搜索文件内容：
 <img src="/media/image-20220414000609625.png" alt="image-20220414000609625" /></li>
</ol>

<h3 id="web-search-config">Alfred Web Search 配置方法</h3>

<p>配置 Web Search 的方法很简单。以 github 为例，首先在搜索框中输入内容，回车：</p>

<p><img src="/media/image-20220417005255179.png" alt="image-20220417005255179" style="zoom:40%;" /></p>

<p>然后观察搜索结果页的 URL，是否包含了输入的搜索内容 <code>imageslr/blog</code>：</p>

<pre><code class="language-text">https://github.com/search?q=imageslr%2Fblog
</code></pre>

<p>把 <code>imageslr/blog</code> 替换为占位符 <code>{query}</code>：</p>

<pre><code class="language-text">https://github.com/search?q={query}
</code></pre>

<p>在 Alfred 中新增自定义搜索 (Preference - Features - Web Search - Add Custom Search)：</p>

<p><img src="/media/image-20220417010021237.png" alt="image-20220417010021237" /></p>

<p>然后就可以使用了：</p>

<p><img src="/media/image-20220417010218015.png" alt="image-20220417010218015" style="zoom:33%;" /></p>

<p>同理，百度搜索的 Search URL 是：</p>

<pre><code>https://www.baidu.com/s?wd={query}
</code></pre>

<p>Google 的 Search URL 是：</p>

<pre><code>https://www.google.com/search?q={query}
</code></pre>

<p>评论区里会不断更新我日常使用的 websearch 配置。</p>

<h3 id="raycast">Raycast vs Alfred</h3>

<p>本文提到的所有 Alfred 的功能，<a href="https://www.raycast.com/">Raycast</a> 都有：</p>

<ol>
  <li>打开 App：Raycast 可以直接给某个 App 绑定快捷键。</li>
  <li>Web Search：在 Raycast 里是 Quicklinks，可以直接粘贴 Alfred 的 Web Search 配置。可以设置 Fallback Result。但不支持重复的 Alias (Keyword)。</li>
  <li>剪贴板历史：支持保存文本、图片、文件。按 Cmd + S 同样能保存 Snippets。但 Snippets 不支持分类保存。</li>
</ol>

<p>其他方面：</p>

<ol>
  <li>快捷键和 Alfred 基本一致，比如按 Cmd + 1、Cmd + 2 等可以快速选择。配置方式和 Alfred 也基本一致，但体验更佳。</li>
  <li>快捷笔记 Floating Notes 功能使用很方便，代替了系统的便签。</li>
  <li>界面现代，交互丝滑。扩展市场应用丰富，安装简便。新手教程友好。</li>
  <li>个人使用完全免费。</li>
</ol>

<p>因此，建议使用 Raycast 代替 Alfred。</p>

<hr />

<p>Raycast 也可以直接替换某些系统插件：</p>

<ul>
  <li><a href="https://rectangleapp.com/">Rectangle</a>：窗口布局工具。Raycast 提供了完全一致的功能 (Window Management)，且可以直接导入 Rectangle 的快捷键 (Presets)。</li>
</ul>

<h3 id="mac-系统自定义快捷键配置方法">Mac 系统自定义快捷键配置方法</h3>

<p id="shortcut-config">以 Typora 为例。点击菜单栏，可以看到这样的操作列表，每个操作标题后面都有对应的快捷键：</p>

<p><img src="/media/image-20220501232842330.png" alt="image-20220501232842330" style="zoom:50%;" /></p>

<p>进入「系统偏好设置 - 键盘 - 快捷键」，选中「App 快捷键」：</p>

<p><img src="/media/image-20220501232927238.png" alt="image-20220501232927238" style="zoom:50%;" /></p>

<p>点击 + 号，选择应用程序，输入菜单中的操作标题，输入自定义快捷键，就可以覆盖应用程序的默认快捷键了：</p>

<p><img src="/media/image-20220501233111588.png" alt="image-20220501233111588" style="zoom:50%;" /></p>

<h3 id="tips">Tips</h3>

<ul>
  <li>搜狗输入法：打开“中英文自动输入空格”</li>
  <li>建议自己申请一个工作专用的 Google 账号，而不是使用公司分配的账号。后者在离职后会被回收。</li>
  <li>富文本如何转为 Markdown：粘贴到 Typora，再复制成 Markdown 即可。</li>
  <li>网页如何转为 Markdown：同上。</li>
</ul>

<div class="ant-alert ant-alert-blue">

  <p class="mb-2"><strong>效率提升方法论系列</strong></p>

  <ul>
    <li><a href="/2021/efficiency-01.html">📥【方法论】我的效率提升方法论 - 通用思维篇 ① 收件箱</a></li>
    <li><a href="/2021/efficiency-02.html">🕖【方法论】我的效率提升方法论 - 通用思维篇 ② 定期回顾</a></li>
    <li><a href="/2021/efficiency-workspace.html">🔖【方法论】我的效率提升方法论 - 通用思维篇 ③ 工作区</a></li>
    <li><a href="/2021/efficiency-okr.html">🎯【方法论】我的效率提升方法论 - 目标管理篇 ④ OKR</a></li>
    <li><a href="/2022/efficiency-gtd.html">🔘【方法论】我的效率提升方法论 - 任务管理篇 ⑤ GTD</a></li>
    <li><a href="/2022/efficiency-workflow.html">⚙️【方法论】我的效率提升方法论 - 工具使用篇 ⑥ Workflow</a></li>
  </ul>

</div>]]></content><author><name>Images。</name></author><summary type="html"><![CDATA[〇、前言]]></summary></entry></feed>