<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>caty</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>http://blog.chcaty.cn/</id>
  <link href="http://blog.chcaty.cn/" rel="alternate"/>
  <link href="http://blog.chcaty.cn/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, caty</rights>
  <subtitle>想到啥就写点啥</subtitle>
  <title>我想探索一下世界</title>
  <updated>2026-04-13T11:00:33.471Z</updated>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="后端开发" scheme="http://blog.chcaty.cn/categories/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="后端开发" scheme="http://blog.chcaty.cn/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>为什么会有做这个计算器的想法？是因为前段时间，女朋友的猫准备开始吃自制猫饭。听起来挺浪漫的，自己给猫做饭。但做了两天我就发现，做饭本身根本不是问题，真正的问题是：<strong>这份食谱，营养到底够不够？</strong></p><p>不是”感觉上健康”就行，而是要有明确答案。蛋白到位了吗，钙磷比对不对，牛磺酸缺口有多大，哪些东西吃多了反而有害。这些问题，靠直觉是回答不了的。</p><span id="more"></span><p>于是我开始查。查着查着发现，宠物营养这块有三套主流标准，每一套都有自己的逻辑。</p><h3 id="先搞清楚：三套标准到底在说什么"><a href="#先搞清楚：三套标准到底在说什么" class="headerlink" title="先搞清楚：三套标准到底在说什么"></a>先搞清楚：三套标准到底在说什么</h3><p>NRC、AAFCO、FEDIAF——这三个名字在宠物营养圈里出现频率极高，但很多人分不清楚它们的区别。我当时也是。</p><table><thead><tr><th align="center"></th><th align="center">NRC</th><th align="center">AAFCO</th><th align="center">FEDIAF</th></tr></thead><tbody><tr><td align="center">全称</td><td align="center">美国国家研究委员会</td><td align="center">美国饲料协会</td><td align="center">欧洲宠物食品工业联合会</td></tr><tr><td align="center">组成</td><td align="center">美国国家科学院旗下非营利机构</td><td align="center">州和地方官员、兽医师、饲料商组成的民间机构</td><td align="center">宠物食品公司、兽医学会、政府部门联合组成</td></tr><tr><td align="center">特点</td><td align="center">依照科学研究制定营养需求，确保标准符合宠物的生理和生长需要</td><td align="center">制定饲料和宠物食品的最低和最高营养标准，控管成分与品质，确保产品标示信息正确可靠</td><td align="center">容纳更多与犬猫健康有关的营养观点，从不同角度说明符合主食的营养标准</td></tr><tr><td align="center">差异</td><td align="center">注重科学研究，标准较为科学化，更新速度较慢</td><td align="center">标准更加实际，注重确保食品标签信息对消费者有实际指导价值，更新较迅速</td><td align="center">将犬猫不同时期所需营养细分，包括幼猫幼犬的前后期，甚至细至不同活动程度的需求</td></tr><tr><td align="center">重点规范</td><td align="center">1. 犬猫营养素需求表 2. 营养素介绍</td><td align="center">1. 犬猫营养素需求表 2. 包装标示原则 3. 检验流程 4. 喂食测试</td><td align="center">1. 犬猫营养素需求表 2. 营养素介绍 3. 体态评分表（BCS 表）</td></tr></tbody></table><p>简单理解：NRC 更像学术底版，AAFCO 更像北美合规框架，FEDIAF 更像欧洲行业指南，分期更细、安全边界也更明确。我在项目里把三套标准都统一换算到每 1000 kcal ME 的口径，支持用户自由切换对照。因为同一份食谱如果在三套标准下都说得过去，结论才更让人放心。</p><h3 id="然后，数据从哪里来"><a href="#然后，数据从哪里来" class="headerlink" title="然后，数据从哪里来"></a>然后，数据从哪里来</h3><p>标准有了，下一个问题是：食材的营养成分怎么查？</p><p>比如鸡心，里面的牛磺酸含量是多少、磷是多少、铁是多少。你去网上一搜，结果往往五花八门，来源不明，数字差异还很大。自制猫饭最怕的就是数据本身不准，却还在上面认认真真地算，最后得到一个看起来”达标”、实际上已经偏掉的结论。</p><p>所以数据这块，从一开始就只考虑官方来源。</p><p>最后选定了两个：<strong>中国食物成分表</strong>和 <strong>USDA FoodData Central</strong>（美国农业部食物数据库）。</p><p>选这两个的理由很直接。中国食物成分表是国内权威的数据来源，对常见食材的覆盖更贴近实际，比如配猫饭常用的鸡胸、猪肝、鸡蛋，用本地数据更可信。USDA 的优势是覆盖范围更广，很多国内数据库里没有的项目，USDA 往往能补上。</p><p>我还基于 USDA 做了一套常用食材数据整合：以 USDA 为基础，结合澳大利亚食品库、日本食品标准成分表、台湾食品成分数据库、新西兰食品成分数据库，以及部分关于食品碘含量、牛磺酸含量的论文，尽量把常用食材的营养成分覆盖全面。营养补剂也补了一部分常用品类。考虑到食材库里仍然会有缺项，工具也支持自定义食材或补剂录入，并参与食谱构成。</p><h3 id="为什么没有直接用现成工具"><a href="#为什么没有直接用现成工具" class="headerlink" title="为什么没有直接用现成工具"></a>为什么没有直接用现成工具</h3><p>市面上不是没有宠物营养计算器，但用下来的体感是：大多数工具要么数据覆盖不全，要么界面很难用，要么标准只支持一种，要么根本不透明——你看到一个”达标”的绿勾，但你不知道它是怎么算出来的。</p><p>最让我难受的是操作不连贯。选食材、记克重、做计算、对照标准，这几件事被切成了好几段。每次只是调整一个食材的克重，都得把整套流程再走一遍。改 10g 肉，不该这么累。</p><p>于是决定自己做一个。</p><h3 id="做的过程里踩了哪些坑"><a href="#做的过程里踩了哪些坑" class="headerlink" title="做的过程里踩了哪些坑"></a>做的过程里踩了哪些坑</h3><p><strong>两个数据库的数据不是一回事</strong></p><p>中国食物成分表和 USDA 数据库都是官方来源，但合并起来并不简单。同一个营养素，两边单位不一定一样，覆盖项目也不完全一致，有些营养素是一边有、一边没有。最麻烦的是，这类问题不会报警，界面看起来一切正常，结果却可能悄悄偏掉。通常只能靠”这个数字看起来不对劲”的直觉，再一层层回查，才知道哪里没对上。</p><p>另外，考虑到后续要持续更新食材数据，我没有把源数据做成固定死的中间格式，而是在程序层做了适配。这样后面只要官方数据源更新，直接替换源文件就能完成无感更新，不需要每次都重新清洗。</p><p><strong>关于食材是单选还是多选</strong></p><p>一开始我把添加食材做成了单个添加。后来自己连续用了几次，发现当食材稍微多一点，这个操作会非常磨人：点一次、加一个，再点一次、再加一个，节奏特别碎。</p><p>后面改成了”先勾到待选区，再批量加入”。这个改动对体验影响很大：可以一次性把食谱要用的食材都选好，再专心调克重，不会总被重复点击打断。</p><h3 id="现在这个工具是什么样的"><a href="#现在这个工具是什么样的" class="headerlink" title="现在这个工具是什么样的"></a>现在这个工具是什么样的</h3><p>核心流程尽量压短：选数据源 → 搜食材勾选到待选区 → 批量加入食谱 → 填克重 → 看营养对标结果。</p><p>不要求用户上来就把所有数字填满，可以先把食材组合搭好，再逐步调整克重。调整一次，结果实时更新，哪项缺、缺多少，页面上直接能看到。</p><p>数据来源支持中国食物成分表、常用食材完整版、USDA 官方数据库，以及用户自定义录入。三套营养标准都可以切换对照。</p><h3 id="还没做完的事"><a href="#还没做完的事" class="headerlink" title="还没做完的事"></a>还没做完的事</h3><p>覆盖的食材还不够全，有些东西查不到就只能手动录入。用起来偶尔也会遇到一些小异常，切换来切换去时状态有时会卡一下。还有一个我一直想解决、但还没解决好的问题：报告出来之后，用户知道某项缺了，却不知道该怎么补。该加什么、加多少、会不会又顶掉别的？光给一张数字表格是不够的，这件事还得继续做。</p><hr><p>这不是一个很宏大的项目，起点只是给一只猫算配方。但做着做着我发现，”算配方”背后涉及的东西比预想中多得多。先做到用起来顺，再做到算得准，然后再想别的。</p><p>如果你也在做自制猫饭，欢迎试试：<a href="https://pet.chcaty.cn/">https://pet.chcaty.cn/</a></p>]]>
    </content>
    <id>http://blog.chcaty.cn/2026/04/13/yin-wei-yi-zhi-mao-wo-zuo-liao-yi-ge-chong-wu-ying-yang-ji-suan-qi/</id>
    <link href="http://blog.chcaty.cn/2026/04/13/yin-wei-yi-zhi-mao-wo-zuo-liao-yi-ge-chong-wu-ying-yang-ji-suan-qi/"/>
    <published>2026-04-13T08:10:36.000Z</published>
    <summary>
      <![CDATA[<h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>为什么会有做这个计算器的想法？是因为前段时间，女朋友的猫准备开始吃自制猫饭。听起来挺浪漫的，自己给猫做饭。但做了两天我就发现，做饭本身根本不是问题，真正的问题是：<strong>这份食谱，营养到底够不够？</strong></p>
<p>不是”感觉上健康”就行，而是要有明确答案。蛋白到位了吗，钙磷比对不对，牛磺酸缺口有多大，哪些东西吃多了反而有害。这些问题，靠直觉是回答不了的。</p>]]>
    </summary>
    <title>因为一只猫，我做了一个宠物营养计算器</title>
    <updated>2026-04-13T11:00:33.471Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="后端开发" scheme="http://blog.chcaty.cn/categories/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <category term="后端开发" scheme="http://blog.chcaty.cn/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>最近在看《深入解析C#（第4版）》这本书，看到了第五章，这一章节是关于异步。之前对异步这个概念只能算是一知半解，了解了它的概念和用法，但是对它的实际场景和为了解决什么问题而诞生的是不太清楚的。于是乎，就和小伙伴之间有了一场讨论。</p><span id="more"></span><h3 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h3><p>一般来说对方法的调用都是同步执行的。例如在线程执行体内，即线程的调用函数中，方法的调用就是同步执行的。如果方法需要很长的时间来完成，比方说从Internet加载数据的方法，调用者线程将被阻塞直到方法调用完成。这时候为了避免调用者线程被阻塞，这时候就需要用到异步编程了。异步编程可以解决线程因为等待独占式任务而导致的阻塞问题。</p><h3 id="探索"><a href="#探索" class="headerlink" title="探索"></a>探索</h3><p>探索过程中，参考了<a href="https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/">《微软官方文档》</a>，<a href="https://enterprisecraftsmanship.com/posts/io-threads-explained/">《I&#x2F;O Threads Explained》</a>。</p><h4 id="例子说明"><a href="#例子说明" class="headerlink" title="例子说明"></a>例子说明</h4><p>官方以一个做早餐的例子来解释了什么叫同步，并行和异步。</p><p>假设做一个早餐需要完成7个步骤：</p><ol><li>倒一杯咖啡。</li><li>加热平底锅，然后煎两个鸡蛋。</li><li>煎三片培根。</li><li>烤两片面包。</li><li>在烤面包上加黄油和果酱。</li><li>倒一杯橙汁。</li></ol><h4 id="同步执行"><a href="#同步执行" class="headerlink" title="同步执行"></a>同步执行</h4><p>同步执行，是指只有完成上一个任务，才会开始下一个任务；同时将阻塞当前线程执行其他操作，直至任务全部完成</p><p>代码例子如下：</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 鉴于我用的是vs2022,可能控制台程序的代码在旧版本的vs上无法直接运行，需要补充对应的main函数</span></span><br><span class="line">MakeBreakfast();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">MakeBreakfast</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">var</span> cup = PourCoffee();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;coffee is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> eggs = FryEggs(<span class="number">2</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;eggs are ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> bacon = FryBacon(<span class="number">3</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;bacon is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> toast = ToastBread(<span class="number">2</span>);</span><br><span class="line">    ApplyButter(toast);</span><br><span class="line">    ApplyJam(toast);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;toast is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> oj = PourOJ();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;oj is ready&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Breakfast is ready!&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Juice <span class="title">PourOJ</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Pouring orange juice&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Juice();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">ApplyJam</span>(<span class="params">Toast toast</span>)</span> =&gt; </span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Putting jam on the toast&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">ApplyButter</span>(<span class="params">Toast toast</span>)</span> =&gt;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Putting butter on the toast&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Toast <span class="title">ToastBread</span>(<span class="params"><span class="built_in">int</span> slices</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> slice = <span class="number">0</span>; slice &lt; slices; slice++)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">&quot;Putting a slice of bread in the toaster&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Start toasting...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Remove toast from toaster&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Toast();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Bacon <span class="title">FryBacon</span>(<span class="params"><span class="built_in">int</span> slices</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">$&quot;putting <span class="subst">&#123;slices&#125;</span> slices of bacon in the pan&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking first side of bacon...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> slice = <span class="number">0</span>; slice &lt; slices; slice++)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">&quot;flipping a slice of bacon&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking the second side of bacon...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Put bacon on plate&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Bacon();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Egg <span class="title">FryEggs</span>(<span class="params"><span class="built_in">int</span> howMany</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Warming the egg pan...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">$&quot;cracking <span class="subst">&#123;howMany&#125;</span> eggs&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking the eggs ...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Put eggs on plate&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Egg();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Coffee <span class="title">PourCoffee</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Pouring coffee&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Coffee();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Juice</span> &#123; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Bacon</span> &#123; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Egg</span> &#123; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Coffee</span> &#123; &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Toast</span> &#123; &#125;</span><br></pre></td></tr></table></figure><p><img src="https://pic.chcaty.cn/synchronous-running.png" alt="同步执行耗时"></p><p>同步执行的总耗时是每个任务耗时的总和。此外，因为是同步执行的原因，在开始制作一份早餐的时候，如果此时又有一份制作早餐的请求过来，是不会开始制作的。如果是客户端程序，使用同步执行耗时时间长的操作，会导致UI线程被阻塞，导致UI线程无法响应用户操作，直至操作完成后，UI线程才相应用户的操作。</p><h4 id="异步执行"><a href="#异步执行" class="headerlink" title="异步执行"></a>异步执行</h4><p>异步执行，是指在遇到await的时候，才需要等待异步操作完成，然后往下执行；但是不会阻塞当前线程执行其他操作。</p><p>代码如下</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> MakeBreakfastAsync();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">MakeBreakfastAsync</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">var</span> cup = PourCoffee();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;coffee is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> eggs = <span class="keyword">await</span> FryEggsAsync(<span class="number">2</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;eggs are ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> bacon = <span class="keyword">await</span> FryBaconAsync(<span class="number">3</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;bacon is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> toast = <span class="keyword">await</span> ToastBreadAsync(<span class="number">2</span>);</span><br><span class="line">    ApplyButter(toast);</span><br><span class="line">    ApplyJam(toast);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;toast is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> oj = PourOJ();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;oj is ready&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Breakfast is ready!&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task&lt;Toast&gt; <span class="title">ToastBreadAsync</span>(<span class="params"><span class="built_in">int</span> slices</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> slice = <span class="number">0</span>; slice &lt; slices; slice++)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">&quot;Putting a slice of bread in the toaster&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Start toasting...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Remove toast from toaster&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">await</span> Task.FromResult(<span class="keyword">new</span> Toast());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Task&lt;Bacon&gt; <span class="title">FryBaconAsync</span>(<span class="params"><span class="built_in">int</span> slices</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">$&quot;putting <span class="subst">&#123;slices&#125;</span> slices of bacon in the pan&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking first side of bacon...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> slice = <span class="number">0</span>; slice &lt; slices; slice++)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">&quot;flipping a slice of bacon&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking the second side of bacon...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Put bacon on plate&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> Task.FromResult(<span class="keyword">new</span> Bacon());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Task&lt;Egg&gt; <span class="title">FryEggsAsync</span>(<span class="params"><span class="built_in">int</span> howMany</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Warming the egg pan...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">$&quot;cracking <span class="subst">&#123;howMany&#125;</span> eggs&quot;</span>);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;cooking the eggs ...&quot;</span>);</span><br><span class="line">    Task.Delay(<span class="number">3000</span>).Wait();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Put eggs on plate&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> Task.FromResult(<span class="keyword">new</span> Egg());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="https://pic.chcaty.cn/asynchronous-running.png" alt="异步执行耗时"></p><p>上面代码只是为了避免堵塞当前的线程，并没有真正用上异步执行的某些关键功能，所以在耗时上是相差不远的；但是这时候如果在接受了一份制作早餐的请求，还未完成的时候，又有一份制作早餐的请求过来，是可能会开始制作另一份早餐的。</p><h4 id="改善后的异步执行"><a href="#改善后的异步执行" class="headerlink" title="改善后的异步执行"></a>改善后的异步执行</h4><p>代码如下</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> MakeBreakfastBetterAsync();</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">MakeBreakfastBetterAsync</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    Coffee cup = PourCoffee();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Coffee is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    Task&lt;Egg&gt; eggsTask = FryEggsAsync(<span class="number">2</span>);</span><br><span class="line">    Task&lt;Bacon&gt; baconTask = FryBaconAsync(<span class="number">3</span>);</span><br><span class="line">    Task&lt;Toast&gt; toastTask = ToastBreadAsync(<span class="number">2</span>);</span><br><span class="line"></span><br><span class="line">    Toast toast = <span class="keyword">await</span> toastTask;</span><br><span class="line">    ApplyButter(toast);</span><br><span class="line">    ApplyJam(toast);</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Toast is ready&quot;</span>);</span><br><span class="line">    Juice oj = PourOJ();</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Oj is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    Egg eggs = <span class="keyword">await</span> eggsTask;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Eggs are ready&quot;</span>);</span><br><span class="line">    Bacon bacon = <span class="keyword">await</span> baconTask;</span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Bacon is ready&quot;</span>);</span><br><span class="line"></span><br><span class="line">    Console.WriteLine(<span class="string">&quot;Breakfast is ready!&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>异步方法的逻辑没有改变，只是调整了一下代码的执行顺序，一开始就调用了三个异步方法，只是在await语句后置了，而不是上面那段代码一样，执行了就在那里等待任务完成，而是会去进行其他的后续操作，直至后续操作需要用到前面任务执行结果的时候，才去获取对应的执行结果，如果没有执行完成就等待执行完成才继续后续的操作。</p><blockquote><p>异步执行并不总是需要另一个线程来执行新任务。并行编程是异步执行的一个子集。</p></blockquote><h4 id="并行编程"><a href="#并行编程" class="headerlink" title="并行编程"></a>并行编程</h4><p>并行编程，调用多个线程，同时去执行任务</p><p>例如：需要制作五份早餐，同步和异步的方法都是需要循环调用相应的MakeBreakfast方法和MakeBreakfastBetterAsync方法五次才能制作完成。而并行编程，也就是多线程，可以一次性创建五个线程，分别制作一份早餐，从而大大缩短了所需要的时间。</p><p>代码如下</p><figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">DateTime beforeDT = DateTime.Now;</span><br><span class="line"><span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i &lt; <span class="number">5</span>; i++)</span><br><span class="line">&#123;</span><br><span class="line">    MakeBreakfast();</span><br><span class="line">&#125;</span><br><span class="line">DateTime afterDT = DateTime.Now;</span><br><span class="line">TimeSpan ts = afterDT.Subtract(beforeDT);</span><br><span class="line">Console.WriteLine(<span class="string">$&quot;同步执行程序耗时: <span class="subst">&#123;ts.TotalMilliseconds&#125;</span>ms&quot;</span>);</span><br><span class="line"></span><br><span class="line">beforeDT = DateTime.Now;</span><br><span class="line"><span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i &lt; <span class="number">5</span>; i++)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">await</span> MakeBreakfastBetterAsync();</span><br><span class="line">&#125;</span><br><span class="line">afterDT = DateTime.Now;</span><br><span class="line">ts = afterDT.Subtract(beforeDT);</span><br><span class="line">Console.WriteLine(<span class="string">$&quot;异步执行程序耗时: <span class="subst">&#123;ts.TotalMilliseconds&#125;</span>ms&quot;</span>);</span><br><span class="line"></span><br><span class="line">beforeDT = DateTime.Now;</span><br><span class="line"><span class="keyword">await</span> MakeBreakfastBetterMultiTask();</span><br><span class="line">afterDT = DateTime.Now;</span><br><span class="line">ts = afterDT.Subtract(beforeDT);</span><br><span class="line">Console.WriteLine(<span class="string">$&quot;并行编程程序耗时: <span class="subst">&#123;ts.TotalMilliseconds&#125;</span>ms&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">MakeBreakfastBetterMultiTask</span>()</span></span><br><span class="line">&#123;</span><br><span class="line">    Task[] tasks = <span class="keyword">new</span> Task[<span class="number">5</span>];</span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i &lt; <span class="number">5</span>; i++)</span><br><span class="line">    &#123;</span><br><span class="line">        tasks[i] = <span class="keyword">new</span> Task((parameter) =&gt; MakeBreakfastBetterAsync().Wait(), <span class="string">&quot;aaa&quot;</span>);</span><br><span class="line">        tasks[i].Start();</span><br><span class="line">    &#125;</span><br><span class="line">    Task.WaitAll(tasks);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行耗时结果如下</p><p><img src="https://pic.chcaty.cn/synchronous-running-time.png" alt="同步运行耗时"></p><p><img src="https://pic.chcaty.cn/asynchronous-running-time.png" alt="异步运行耗时"></p><p><img src="https://pic.chcaty.cn/parallel-running-time.png" alt="并行运行耗时"></p><p>相比之下，显然能看出来之间的运行耗时差别还是有点大的。</p><h3 id="一个通俗的例子"><a href="#一个通俗的例子" class="headerlink" title="一个通俗的例子"></a>一个通俗的例子</h3><p>程序就像一个餐馆，线程就像餐馆里面已有的厨师，CPU就是调度厨师的厨师长，假设餐馆开业了，厨师长只带了5个厨师，餐馆接到的订单有8份，同步执行就是5个厨师分别处理5个订单后，这期间，他们会专心的去完成订单的菜，而无视其他的事情，直到完成订单，厨师长才会分配新的订单给他们；异步执行则是5个厨师在处理5个订单的期间，如果厨师长发现他们有人处于空闲状态，就会安排他们去执行剩下3个订单，如果收到等待中的订单可以继续操作时，厨师长会抽调厨师继续完成订单，从而增加了餐馆处理订单的能力。而并行编程则是餐馆开业的时候，告诉了厨师长，需要8个厨师；厨师长就带来了相应数量的厨师来处理订单。</p><p>这就是这两天，我对同步，异步和并行之间的感悟。如有不对，敬请指正！</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/12/03/guan-yu-async-he-await-de-tan-tao/</id>
    <link href="http://blog.chcaty.cn/2021/12/03/guan-yu-async-he-await-de-tan-tao/"/>
    <published>2021-12-03T03:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>最近在看《深入解析C#（第4版）》这本书，看到了第五章，这一章节是关于异步。之前对异步这个概念只能算是一知半解，了解了它的概念和用法，但是对它的实际场景和为了解决什么问题而诞生的是不太清楚的。于是乎，就和小伙伴之间有了一场讨论。</p>]]>
    </summary>
    <title>关于async和await的探讨</title>
    <updated>2026-04-13T10:24:15.865Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>今天再来讲一个并发安全的高级数据结构：sync.Map。众所周知，Go 语言自带的字典类型map并不是并发安全的。</p><span id="more"></span><h3 id="前导知识：并发安全字典诞生史"><a href="#前导知识：并发安全字典诞生史" class="headerlink" title="前导知识：并发安全字典诞生史"></a>前导知识：并发安全字典诞生史</h3><p>换句话说，在同一时间段内，让不同 goroutine 中的代码，对同一个字典进行读写操作是不安全的。字典值本身可能会因这些操作而产生混乱，相关的程序也可能会因此发生不可预知的问题。</p><p>在sync.Map出现之前，我们如果要实现并发安全的字典，就只能自行构建。不过，这其实也不是什么麻烦事，使用 sync.Mutex或sync.RWMutex，再加上原生的map就可以轻松地做到。</p><p>GitHub 网站上已经有很多库提供了类似的数据结构。我在《Go 并发编程实战》的第 2 版中也提供了一个比较完整的并发安全字典的实现。它的性能比同类的数据结构还要好一些，因为它在很大程度上有效地避免了对锁的依赖。</p><p>尽管已经有了不少的参考实现，Go 语言爱好者们还是希望 Go 语言官方能够发布一个标准的并发安全字典。</p><p>经过大家多年的建议和吐槽，Go 语言官方终于在 2017 年发布的 Go 1.9 中，正式加入了并发安全的字典类型sync.Map。</p><p>这个字典类型提供了一些常用的键值存取操作方法，并保证了这些操作的并发安全。同时，它的存、取、删等操作都可以基本保证在常数时间内执行完毕。换句话说，它们的算法复杂度与map类型一样都是O(1)的。</p><p>在有些时候，与单纯使用原生map和互斥锁的方案相比，使用sync.Map可以显著地减少锁的争用。sync.Map本身虽然也用到了锁，但是，它其实在尽可能地避免使用锁。</p><p>我们都知道，使用锁就意味着要把一些并发的操作强制串行化。这往往会降低程序的性能，尤其是在计算机拥有多个 CPU 核心的情况下。</p><p>因此，我们常说，能用原子操作就不要用锁，不过这很有局限性，毕竟原子只能对一些基本的数据类型提供支持。</p><p>无论在何种场景下使用sync.Map，我们都需要注意，与原生map明显不同，它只是 Go 语言标准库中的一员，而不是语言层面的东西。也正因为这一点，Go 语言的编译器并不会对它的键和值，进行特殊的类型检查。</p><p>如果你看过sync.Map的文档或者实际使用过它，那么就一定会知道，它所有的方法涉及的键和值的类型都是interface{}，也就是空接口，这意味着可以包罗万象。所以，我们必须在程序中自行保证它的键类型和值类型的正确性。</p><h3 id="今天的问题是：并发安全字典对键的类型有要求吗？"><a href="#今天的问题是：并发安全字典对键的类型有要求吗？" class="headerlink" title="今天的问题是：并发安全字典对键的类型有要求吗？"></a>今天的问题是：并发安全字典对键的类型有要求吗？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>有要求。键的实际类型不能是函数类型、字典类型和切片类型。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>我们都知道，Go 语言的原生字典的键类型不能是函数类型、字典类型和切片类型。</p><p>由于并发安全字典内部使用的存储介质正是原生字典，又因为它使用的原生字典键类型也是可以包罗万象的interface{}；所以，我们绝对不能带着任何实际类型为函数类型、字典类型或切片类型的键值去操作并发安全字典。</p><p>由于这些键值的实际类型只有在程序运行期间才能够确定，所以 Go 语言编译器是无法在编译期对它们进行检查的，不正确的键值实际类型肯定会引发 panic。</p><p>因此，我们在这里首先要做的一件事就是：一定不要违反上述规则。我们应该在每次操作并发安全字典的时候，都去显式地检查键值的实际类型。无论是存、取还是删，都应该如此。</p><p>当然，更好的做法是，把针对同一个并发安全字典的这几种操作都集中起来，然后统一地编写检查代码。除此之外，把并发安全字典封装在一个结构体类型中，往往是一个很好的选择。</p><p>总之，我们必须保证键的类型是可比较的（或者说可判等的）。如果你实在拿不准，那么可以先通过调用reflect.TypeOf函数得到一个键值对应的反射类型值（即：reflect.Type类型的值），然后再调用这个值的Comparable方法，得到确切的判断结果。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：怎样保证并发安全字典中的键和值的类型正确性？（方案一）"><a href="#问题-1：怎样保证并发安全字典中的键和值的类型正确性？（方案一）" class="headerlink" title="问题 1：怎样保证并发安全字典中的键和值的类型正确性？（方案一）"></a>问题 1：怎样保证并发安全字典中的键和值的类型正确性？（方案一）</h4><p>简单地说，可以使用类型断言表达式或者反射操作来保证它们的类型正确性。</p><p>为了进一步明确并发安全字典中键值的实际类型，这里大致有两种方案可选。</p><p><strong>第一种方案是，让并发安全字典只能存储某个特定类型的键。</strong></p><p>比如，指定这里的键只能是int类型的，或者只能是字符串，又或是某类结构体。一旦完全确定了键的类型，你就可以在进行存、取、删操作的时候，使用类型断言表达式去对键的类型做检查了。</p><p>一般情况下，这种检查并不繁琐。而且，你要是把并发安全字典封装在一个结构体类型里面，那就更加方便了。你这时完全可以让 Go 语言编译器帮助你做类型检查。请看下面的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> IntStrMap <span class="keyword">struct</span> &#123;</span><br><span class="line"> m sync.Map</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(iMap *IntStrMap)</span></span> Delete(key <span class="type">int</span>) &#123;</span><br><span class="line"> iMap.m.Delete(key)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(iMap *IntStrMap)</span></span> Load(key <span class="type">int</span>) (value <span class="type">string</span>, ok <span class="type">bool</span>) &#123;</span><br><span class="line"> v, ok := iMap.m.Load(key)</span><br><span class="line"> <span class="keyword">if</span> v != <span class="literal">nil</span> &#123;</span><br><span class="line">  value = v.(<span class="type">string</span>)</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(iMap *IntStrMap)</span></span> LoadOrStore(key <span class="type">int</span>, value <span class="type">string</span>) (actual <span class="type">string</span>, loaded <span class="type">bool</span>) &#123;</span><br><span class="line"> a, loaded := iMap.m.LoadOrStore(key, value)</span><br><span class="line"> actual = a.(<span class="type">string</span>)</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(iMap *IntStrMap)</span></span> Range(f <span class="function"><span class="keyword">func</span><span class="params">(key <span class="type">int</span>, value <span class="type">string</span>)</span></span> <span class="type">bool</span>) &#123;</span><br><span class="line"> f1 := <span class="function"><span class="keyword">func</span><span class="params">(key, value <span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> f(key.(<span class="type">int</span>), value.(<span class="type">string</span>))</span><br><span class="line"> &#125;</span><br><span class="line"> iMap.m.Range(f1)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(iMap *IntStrMap)</span></span> Store(key <span class="type">int</span>, value <span class="type">string</span>) &#123;</span><br><span class="line"> iMap.m.Store(key, value)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如上所示，我编写了一个名为IntStrMap的结构体类型，它代表了键类型为int、值类型为string的并发安全字典。在这个结构体类型中，只有一个sync.Map类型的字段m。并且，这个类型拥有的所有方法，都与sync.Map类型的方法非常类似。</p><p>两者对应的方法名称完全一致，方法签名也非常相似，只不过，与键和值相关的那些参数和结果的类型不同而已。在IntStrMap类型的方法签名中，明确了键的类型为int，且值的类型为string。</p><p>显然，这些方法在接受键和值的时候，就不用再做类型检查了。另外，这些方法在从m中取出键和值的时候，完全不用担心它们的类型会不正确，因为它的正确性在当初存入的时候，就已经由 Go 语言编译器保证了。</p><p>稍微总结一下。第一种方案适用于我们可以完全确定键和值的具体类型的情况。在这种情况下，我们可以利用 Go 语言编译器去做类型检查，并用类型断言表达式作为辅助，就像IntStrMap那样。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/30/go-yu-yan-he-xin-36-jiang-bing-fa-an-quan-zi-dian-sync.map-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/30/go-yu-yan-he-xin-36-jiang-bing-fa-an-quan-zi-dian-sync.map-shang/"/>
    <published>2021-11-30T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>今天再来讲一个并发安全的高级数据结构：sync.Map。众所周知，Go 语言自带的字典类型map并不是并发安全的。</p>]]>
    </summary>
    <title>Go语言核心36讲-并发安全字典sync.Map （上）</title>
    <updated>2026-04-13T10:24:15.852Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p><strong>使用WaitGroup值的时候，我们最好用“先统一Add，再并发Done，最后Wait”的标准模式来构建协作流程。</strong></p><p>如果在调用该值的Wait方法的同时，为了增大其计数器的值，而并发地调用该值的Add方法，那么就很可能会引发 panic。</p><p>这就带来了一个问题，如果我们不能在一开始就确定执行子任务的 goroutine 的数量，那么使用WaitGroup值来协调它们和分发子任务的 goroutine，就是有一定风险的。一个解决方案是：分批地启用执行子任务的 goroutine。</p><span id="more"></span><h3 id="前导内容：WaitGroup-值补充知识"><a href="#前导内容：WaitGroup-值补充知识" class="headerlink" title="前导内容：WaitGroup 值补充知识"></a>前导内容：WaitGroup 值补充知识</h3><p>我们都知道，WaitGroup值是可以被复用的，但需要保证其计数周期的完整性。尤其是涉及对其Wait方法调用的时候，它的下一个计数周期必须要等到，与当前计数周期对应的那个Wait方法调用完成之后，才能够开始。</p><p>我在前面提到的可能会引发 panic 的情况，就是由于没有遵循这条规则而导致的。</p><p>只要我们在严格遵循上述规则的前提下，分批地启用执行子任务的 goroutine，就肯定不会有问题。具体的实现方式有不少，其中最简单的方式就是使用for循环来作为辅助。这里的代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">coordinateWithWaitGroup</span><span class="params">()</span></span> &#123;</span><br><span class="line"> total := <span class="number">12</span></span><br><span class="line"> stride := <span class="number">3</span></span><br><span class="line"> <span class="keyword">var</span> num <span class="type">int32</span></span><br><span class="line"> fmt.Printf(<span class="string">&quot;The number: %d [with sync.WaitGroup]\n&quot;</span>, num)</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= total; i = i + stride &#123;</span><br><span class="line">  wg.Add(stride)</span><br><span class="line">  <span class="keyword">for</span> j := <span class="number">0</span>; j &lt; stride; j++ &#123;</span><br><span class="line">   <span class="keyword">go</span> addNum(&amp;num, i+j, wg.Done)</span><br><span class="line">  &#125;</span><br><span class="line">  wg.Wait()</span><br><span class="line"> &#125;</span><br><span class="line"> fmt.Println(<span class="string">&quot;End.&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// addNum 用于原子地增加一次numP所指的变量的值。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">addNum</span><span class="params">(numP *<span class="type">int32</span>, id <span class="type">int</span>, deferFunc <span class="keyword">func</span>()</span></span>) &#123;</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        deferFunc()</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; ; i++ &#123;</span><br><span class="line">        currNum := atomic.LoadInt32(numP)</span><br><span class="line">        newNum := currNum + <span class="number">1</span></span><br><span class="line">        time.Sleep(time.Millisecond * <span class="number">200</span>)</span><br><span class="line">        <span class="keyword">if</span> atomic.CompareAndSwapInt32(numP, currNum, newNum) &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;The number: %d [%d-%d]\n&quot;</span>, newNum, id, i)</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">//fmt.Printf(&quot;The CAS operation failed. [%d-%d]\n&quot;, id, i)</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们可以看到，经过coordinateWithWaitGroup函数，循环地使用了由变量wg代表的WaitGroup值。它运用的依然是“先统一Add，再并发Done，最后Wait”的这种模式，只不过它利用for语句，对此进行了复用。</p><h3 id="今天的问题是：怎样使用context包中的程序实体，实现一对多的-goroutine-协作流程？"><a href="#今天的问题是：怎样使用context包中的程序实体，实现一对多的-goroutine-协作流程？" class="headerlink" title="今天的问题是：怎样使用context包中的程序实体，实现一对多的 goroutine 协作流程？"></a>今天的问题是：怎样使用context包中的程序实体，实现一对多的 goroutine 协作流程？</h3><p>更具体地说，我需要你编写一个名为coordinateWithContext的函数。这个函数应该具有上面coordinateWithWaitGroup函数相同的功能。</p><p>显然，你不能再使用sync.WaitGroup了，而要用context包中的函数和Context类型作为实现工具。这里注意一点，是否分批启用执行子任务的 goroutine 其实并不重要。</p><h4 id="参考答案"><a href="#参考答案" class="headerlink" title="参考答案"></a>参考答案</h4><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">coordinateWithContext</span><span class="params">()</span></span> &#123;</span><br><span class="line"> total := <span class="number">12</span></span><br><span class="line"> <span class="keyword">var</span> num <span class="type">int32</span></span><br><span class="line"> fmt.Printf(<span class="string">&quot;The number: %d [with context.Context]\n&quot;</span>, num)</span><br><span class="line"> cxt, cancelFunc := context.WithCancel(context.Background())</span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= total; i++ &#123;</span><br><span class="line">  <span class="keyword">go</span> addNum(&amp;num, i, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">   <span class="keyword">if</span> atomic.LoadInt32(&amp;num) == <span class="type">int32</span>(total) &#123;</span><br><span class="line">    cancelFunc()</span><br><span class="line">   &#125;</span><br><span class="line">  &#125;)</span><br><span class="line"> &#125;</span><br><span class="line"> &lt;-cxt.Done()</span><br><span class="line"> fmt.Println(<span class="string">&quot;End.&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个函数体中，我先后调用了context.Background函数和context.WithCancel函数，并得到了一个可撤销的context.Context类型的值（由变量cxt代表），以及一个context.CancelFunc类型的撤销函数（由变量cancelFunc代表）。</p><p>在后面那条唯一的for语句中，我在每次迭代中都通过一条go语句，异步地调用addNum函数，调用的总次数只依据了total变量的值。</p><p>请注意我给予addNum函数的最后一个参数值。它是一个匿名函数，其中只包含了一条if语句。这条if语句会“原子地”加载num变量的值，并判断它是否等于total变量的值。</p><p>如果两个值相等，那么就调用cancelFunc函数。其含义是，如果所有的addNum函数都执行完毕，那么就立即通知分发子任务的 goroutine。</p><p>这里分发子任务的 goroutine，即为执行coordinateWithContext函数的 goroutine。它在执行完for语句后，会立即调用cxt变量的Done函数，并试图针对该函数返回的通道，进行接收操作。</p><p>由于一旦cancelFunc函数被调用，针对该通道的接收操作就会马上结束，所以，这样做就可以实现“等待所有的addNum函数都执行完毕”的功能。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>context.Context类型（以下简称Context类型）是在 Go 1.7 发布时才被加入到标准库的。而后，标准库中的很多其他代码包都为了支持它而进行了扩展，包括：os&#x2F;exec包、net包、database&#x2F;sql包，以及runtime&#x2F;pprof包和runtime&#x2F;trace包，等等。</p><p>Context类型之所以受到了标准库中众多代码包的积极支持，主要是因为它是一种非常通用的同步工具。它的值不但可以被任意地扩散，而且还可以被用来传递额外的信息和信号。</p><p>更具体地说，Context类型可以提供一类代表上下文的值。此类值是并发安全的，也就是说它可以被传播给多个 goroutine。</p><p>由于Context类型实际上是一个接口类型，而context包中实现该接口的所有私有类型，都是基于某个数据类型的指针类型，所以，如此传播并不会影响该类型值的功能和安全。</p><p>Context类型的值（以下简称Context值）是可以繁衍的，这意味着我们可以通过一个Context值产生出任意个子值。这些子值可以携带其父值的属性和数据，也可以响应我们通过其父值传达的信号。</p><p>正因为如此，所有的Context值共同构成了一颗代表了上下文全貌的树形结构。这棵树的树根（或者称上下文根节点）是一个已经在context包中预定义好的Context值，它是全局唯一的。通过调用context.Background函数，我们就可以获取到它（我在coordinateWithContext函数中就是这么做的）。</p><p>这里注意一下，这个上下文根节点仅仅是一个最基本的支点，它不提供任何额外的功能。也就是说，它既不可以被撤销（cancel），也不能携带任何数据。</p><p>除此之外，context包中还包含了四个用于繁衍Context值的函数，即：WithCancel、WithDeadline、WithTimeout和WithValue。</p><p>这些函数的第一个参数的类型都是context.Context，而名称都为parent。顾名思义，这个位置上的参数对应的都是它们将会产生的Context值的父值。</p><p>WithCancel函数用于产生一个可撤销的parent的子值。在coordinateWithContext函数中，我通过调用该函数，获得了一个衍生自上下文根节点的Context值，和一个用于触发撤销信号的函数。</p><p>而WithDeadline函数和WithTimeout函数则都可以被用来产生一个会定时撤销的parent的子值。至于WithValue函数，我们可以通过调用它，产生一个会携带额外数据的parent的子值。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：“可撤销的”在context包中代表着什么？“撤销”一个Context值又意味着什么？"><a href="#问题-1：“可撤销的”在context包中代表着什么？“撤销”一个Context值又意味着什么？" class="headerlink" title="问题 1：“可撤销的”在context包中代表着什么？“撤销”一个Context值又意味着什么？"></a>问题 1：“可撤销的”在context包中代表着什么？“撤销”一个Context值又意味着什么？</h4><p>我相信很多初识context包的 Go 程序开发者，都会有这样的疑问。确实，“可撤销的”（cancelable）这个词在这里是比较抽象的，很容易让人迷惑。我这里再来解释一下。</p><p>这需要从Context类型的声明讲起。这个接口中有两个方法与“撤销”息息相关。Done方法会返回一个元素类型为struct{}的接收通道。不过，这个接收通道的用途并不是传递元素值，而是让调用方去感知“撤销”当前Context值的那个信号。</p><p>一旦当前的Context值被撤销，这里的接收通道就会被立即关闭。我们都知道，对于一个未包含任何元素值的通道来说，它的关闭会使任何针对它的接收操作立即结束。</p><p>正因为如此，在coordinateWithContext函数中，基于调用表达式cxt.Done()的接收操作，才能够起到感知撤销信号的作用。</p><p>除了让Context值的使用方感知到撤销信号，让它们得到“撤销”的具体原因，有时也是很有必要的。后者即是Context类型的Err方法的作用。该方法的结果是error类型的，并且其值只可能等于context.Canceled变量的值，或者context.DeadlineExceeded变量的值。</p><p>前者用于表示手动撤销，而后者则代表：由于我们给定的过期时间已到，而导致的撤销。</p><p>你可能已经感觉到了，对于Context值来说，“撤销”这个词如果当名词讲，指的其实就是被用来表达“撤销”状态的信号；如果当动词讲，指的就是对撤销信号的传达；而“可撤销的”指的则是具有传达这种撤销信号的能力。</p><p>我在前面讲过，当我们通过调用context.WithCancel函数产生一个可撤销的Context值时，还会获得一个用于触发撤销信号的函数。</p><p>通过调用这个函数，我们就可以触发针对这个Context值的撤销信号。一旦触发，撤销信号就会立即被传达给这个Context值，并由它的Done方法的结果值（一个接收通道）表达出来。</p><p>撤销函数只负责触发信号，而对应的可撤销的Context值也只负责传达信号，它们都不会去管后边具体的“撤销”操作。实际上，我们的代码可以在感知到撤销信号之后，进行任意的操作，Context值对此并没有任何的约束。</p><p>最后，若再深究的话，这里的“撤销”最原始的含义其实就是，终止程序针对某种请求（比如 HTTP 请求）的响应，或者取消对某种指令（比如 SQL 指令）的处理。这也是 Go 语言团队在创建context代码包，和Context类型时的初衷。</p><h4 id="问题-2：撤销信号是如何在上下文树中传播的？"><a href="#问题-2：撤销信号是如何在上下文树中传播的？" class="headerlink" title="问题 2：撤销信号是如何在上下文树中传播的？"></a>问题 2：撤销信号是如何在上下文树中传播的？</h4><p>我在前面讲了，context包中包含了四个用于繁衍Context值的函数。其中的WithCancel、WithDeadline和WithTimeout都是被用来基于给定的Context值产生可撤销的子值的。</p><p>context包的WithCancel函数在被调用后会产生两个结果值。第一个结果值就是那个可撤销的Context值，而第二个结果值则是用于触发撤销信号的函数。</p><p>在撤销函数被调用之后，对应的Context值会先关闭它内部的接收通道，也就是它的Done方法会返回的那个通道。</p><p>然后，它会向它的所有子值（或者说子节点）传达撤销信号。这些子值会如法炮制，把撤销信号继续传播下去。最后，这个Context值会断开它与其父值之间的关联。</p><p><img src="https://static001.geekbang.org/resource/image/a8/9e/a801f8f2b5e89017ec2857bc1815fc9e.png" alt="在上下文树中传播撤销信号"></p><p>我们通过调用context包的WithDeadline函数或者WithTimeout函数生成的Context值也是可撤销的。它们不但可以被手动撤销，还会依据在生成时被给定的过期时间，自动地进行定时撤销。这里定时撤销的功能是借助它们内部的计时器来实现的。</p><p>当过期时间到达时，这两种Context值的行为与Context值被手动撤销时的行为是几乎一致的，只不过前者会在最后停止并释放掉其内部的计时器。</p><p>最后要注意，通过调用context.WithValue函数得到的Context值是不可撤销的。撤销信号在被传播时，若遇到它们则会直接跨过，并试图将信号直接传给它们的子值。</p><h4 id="问题-3：怎样通过Context值携带数据？怎样从中获取数据？"><a href="#问题-3：怎样通过Context值携带数据？怎样从中获取数据？" class="headerlink" title="问题 3：怎样通过Context值携带数据？怎样从中获取数据？"></a>问题 3：怎样通过Context值携带数据？怎样从中获取数据？</h4><p>既然谈到了context包的WithValue函数，我们就来说说Context值携带数据的方式。</p><p>WithValue函数在产生新的Context值（以下简称含数据的Context值）的时候需要三个参数，即：父值、键和值。与“字典对于键的约束”类似，这里键的类型必须是可判等的。</p><p>原因很简单，当我们从中获取数据的时候，它需要根据给定的键来查找对应的值。不过，这种Context值并不是用字典来存储键和值的，后两者只是被简单地存储在前者的相应字段中而已。</p><p>Context类型的Value方法就是被用来获取数据的。在我们调用含数据的Context值的Value方法时，它会先判断给定的键，是否与当前值中存储的键相等，如果相等就把该值中存储的值直接返回，否则就到其父值中继续查找。</p><p>如果其父值中仍然未存储相等的键，那么该方法就会沿着上下文根节点的方向一路查找下去。</p><p>注意，除了含数据的Context值以外，其他几种Context值都是无法携带数据的。因此，Context值的Value方法在沿路查找的时候，会直接跨过那几种值。</p><p>如果我们调用的Value方法的所属值本身就是不含数据的，那么实际调用的就将会是其父辈或祖辈的Value方法。这是由于这几种Context值的实际类型，都属于结构体类型，并且它们都是通过“将其父值嵌入到自身”，来表达父子关系的。</p><p>最后，提醒一下，Context接口并没有提供改变数据的方法。因此，在通常情况下，我们只能通过在上下文树中添加含数据的Context值来存储新的数据，或者通过撤销此种值的父值丢弃掉相应的数据。如果你存储在这里的数据可以从外部改变，那么必须自行保证安全。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/29/go-yu-yan-he-xin-36-jiang-context.context-lei-xing/</id>
    <link href="http://blog.chcaty.cn/2021/11/29/go-yu-yan-he-xin-36-jiang-context.context-lei-xing/"/>
    <published>2021-11-29T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p><strong>使用WaitGroup值的时候，我们最好用“先统一Add，再并发Done，最后Wait”的标准模式来构建协作流程。</strong></p>
<p>如果在调用该值的Wait方法的同时，为了增大其计数器的值，而并发地调用该值的Add方法，那么就很可能会引发 panic。</p>
<p>这就带来了一个问题，如果我们不能在一开始就确定执行子任务的 goroutine 的数量，那么使用WaitGroup值来协调它们和分发子任务的 goroutine，就是有一定风险的。一个解决方案是：分批地启用执行子任务的 goroutine。</p>]]>
    </summary>
    <title>Go语言核心36讲-context.Context类型</title>
    <updated>2026-04-13T10:24:15.849Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>sync.Pool类型可以被称为临时对象池，它的值可以被用来存储临时的对象。与 Go 语言的很多同步工具一样，sync.Pool类型也属于结构体类型，它的值在被真正使用之后，就不应该再被复制了。</p><p>这里的“临时对象”的意思是：不需要持久使用的某一类值。这类值对于程序来说可有可无，但如果有的话会明显更好。它们的创建和销毁可以在任何时候发生，并且完全不会影响到程序的功能。</p><p>同时，它们也应该是无需被区分的，其中的任何一个值都可以代替另一个。如果你的某类值完全满足上述条件，那么你就可以把它们存储到临时对象池中。</p><span id="more"></span><p>你可能已经想到了，我们可以把临时对象池当作针对某种数据的缓存来用。实际上，在我看来，临时对象池最主要的用途就在于此。</p><p>sync.Pool类型只有两个方法——Put和Get。Put 用于在当前的池中存放临时对象，它接受一个interface{}类型的参数；而 Get 则被用于从当前的池中获取临时对象，它会返回一个interface{}类型的值。</p><p>更具体地说，这个类型的Get方法可能会从当前的池中删除掉任何一个值，然后把这个值作为结果返回。如果此时当前的池中没有任何值，那么这个方法就会使用当前池的New字段创建一个新值，并直接将其返回。</p><p>sync.Pool类型的New字段代表着创建临时对象的函数。它的类型是没有参数但有唯一结果的函数类型，即：func() interface{}。</p><p>这个函数是Get方法最后的临时对象获取手段。Get方法如果到了最后，仍然无法获取到一个值，那么就会调用该函数。该函数的结果值并不会被存入当前的临时对象池中，而是直接返回给Get方法的调用方。</p><p>这个函数是Get方法最后的临时对象获取手段。Get方法如果到了最后，仍然无法获取到一个值，那么就会调用该函数。该函数的结果值并不会被存入当前的临时对象池中，而是直接返回给Get方法的调用方。</p><p>这里的New字段的实际值需要我们在初始化临时对象池的时候就给定。否则，在我们调用它的Get方法的时候就有可能会得到nil。所以，sync.Pool类型并不是开箱即用的。不过，这个类型也就只有这么一个公开的字段，因此初始化起来也并不麻烦。</p><p>举个例子。标准库代码包fmt就使用到了sync.Pool类型。这个包会创建一个用于缓存某类临时对象的sync.Pool类型值，并将这个值赋给一个名为ppFree的变量。这类临时对象可以识别、格式化和暂存需要打印的内容。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> ppFree = sync.Pool&#123;</span><br><span class="line"> New: <span class="function"><span class="keyword">func</span><span class="params">()</span></span> <span class="keyword">interface</span>&#123;&#125; &#123; <span class="keyword">return</span> <span class="built_in">new</span>(pp) &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>临时对象池ppFree的New字段在被调用的时候，总是会返回一个全新的pp类型值的指针（即临时对象）。这就保证了ppFree的Get方法总能返回一个可以包含需要打印内容的值。</p><p>pp类型是fmt包中的私有类型，它有很多实现了不同功能的方法。不过，这里的重点是，它的每一个值都是独立的、平等的和可重用的。</p><blockquote><p>更具体地说，这些对象既互不干扰，又不会受到外部状态的影响。它们几乎只针对某个需要打印内容的缓冲区而已。由于fmt包中的代码在真正使用这些临时对象之前，总是会先对其进行重置，所以它们并不在意取到的是哪一个临时对象。这就是临时对象的平等性的具体体现。</p></blockquote><p>另外，这些代码在使用完临时对象之后，都会先抹掉其中已缓冲的内容，然后再把它存放到ppFree中。这样就为重用这类临时对象做好了准备。</p><p>众所周知的fmt.Println、fmt.Printf等打印函数都是如此使用ppFree，以及其中的临时对象的。因此，在程序同时执行很多的打印函数调用的时候，ppFree可以及时地把它缓存的临时对象提供给它们，以加快执行的速度。</p><p>而当程序在一段时间内不再执行打印函数调用时，ppFree中的临时对象又能够被及时地清理掉，以节省内存空间。</p><p>显然，在这个维度上，临时对象池可以帮助程序实现可伸缩性。这就是它的最大价值。</p><h3 id="今天的问题是：为什么说临时对象池中的值会被及时地清理掉？"><a href="#今天的问题是：为什么说临时对象池中的值会被及时地清理掉？" class="headerlink" title="今天的问题是：为什么说临时对象池中的值会被及时地清理掉？"></a>今天的问题是：为什么说临时对象池中的值会被及时地清理掉？</h3><p>更具体地说，我需要你编写一个名为coordinateWithContext的函数。这个函数应该具有上面coordinateWithWaitGroup函数相同的功能。</p><p>显然，你不能再使用sync.WaitGroup了，而要用context包中的函数和Context类型作为实现工具。这里注意一点，是否分批启用执行子任务的 goroutine 其实并不重要。</p><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>因为，Go 语言运行时系统中的垃圾回收器，所以在每次开始执行之前，都会对所有已创建的临时对象池中的值进行全面地清除。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>sync包在被初始化的时候，会向 Go 语言运行时系统注册一个函数，这个函数的功能就是清除所有已创建的临时对象池中的值。我们可以把它称为池清理函数。</p><p>一旦池清理函数被注册到了 Go 语言运行时系统，后者在每次即将执行垃圾回收时就都会执行前者。</p><p>另外，在sync包中还有一个包级私有的全局变量。这个变量代表了当前的程序中使用的所有临时对象池的汇总，它是元素类型为*sync.Pool的切片。我们可以称之为池汇总列表。</p><p>通常，在一个临时对象池的Put方法或Get方法第一次被调用的时候，这个池就会被添加到池汇总列表中。正因为如此，池清理函数总是能访问到所有正在被真正使用的临时对象池。</p><p>更具体地说，池清理函数会遍历池汇总列表。对于其中的每一个临时对象池，它都会先将池中所有的私有临时对象和共享临时对象列表都置为nil，然后再把这个池中的所有本地池列表都销毁掉。</p><p>最后，池清理函数会把池汇总列表重置为空的切片。如此一来，这些池中存储的临时对象就全部被清除干净了。</p><p>如果临时对象池以外的代码再无对它们的引用，那么在稍后的垃圾回收过程中，这些临时对象就会被当作垃圾销毁掉，它们占用的内存空间也会被回收以备他用。</p><p>以上，就是我对临时对象清理的进一步说明。首先需要记住的是，池清理函数和池汇总列表的含义，以及它们起到的关键作用。一旦理解了这些，那么在有人问到你这个问题的时候，你应该就可以从容地应对了。</p><p>不过，我们在这里还碰到了几个新的词，比如：私有临时对象、共享临时对象列表和本地池。这些都代表着什么呢？这就涉及了下面的问题。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：临时对象池存储值所用的数据结构是怎样的？"><a href="#问题-1：临时对象池存储值所用的数据结构是怎样的？" class="headerlink" title="问题 1：临时对象池存储值所用的数据结构是怎样的？"></a>问题 1：临时对象池存储值所用的数据结构是怎样的？</h4><p>在临时对象池中，有一个多层的数据结构。正因为有了它的存在，临时对象池才能够非常高效地存储大量的值。</p><p>这个数据结构的顶层，我们可以称之为本地池列表，不过更确切地说，它是一个数组。这个列表的长度，总是与 Go 语言调度器中的 P 的数量相同。</p><p>还记得吗？Go 语言调度器中的 P 是 processor 的缩写，它指的是一种可以承载若干个 G、且能够使这些 G 适时地与 M 进行对接，并得到真正运行的中介。</p><p>这里的 G 正是 goroutine 的缩写，而 M 则是 machine 的缩写，后者指代的是系统级的线程。正因为有了 P 的存在，G 和 M 才能够进行灵活、高效的配对，从而实现强大的并发编程模型。</p><p>P 存在的一个很重要的原因是为了分散并发程序的执行压力，而让临时对象池中的本地池列表的长度与 P 的数量相同的主要原因也是分散压力。这里所说的压力包括了存储和性能两个方面。在说明它们之前，我们先来探索一下临时对象池中的那个数据结构</p><p>。在本地池列表中的每个本地池都包含了三个字段（或者说组件），它们是：存储私有临时对象的字段private、代表了共享临时对象列表的字段shared，以及一个sync.Mutex类型的嵌入字段。<br><img src="https://static001.geekbang.org/resource/image/82/22/825cae64e0a879faba34c0a157b7ca22.png" alt="sync.Pool 中的本地池与各个 G 的对应关系"></p><p>实际上，每个本地池都对应着一个 P。我们都知道，一个 goroutine 要想真正运行就必须先与某个 P 产生关联。也就是说，一个正在运行的 goroutine 必然会关联着某个 P。</p><p>在程序调用临时对象池的Put方法或Get方法的时候，总会先试图从该临时对象池的本地池列表中，获取与之对应的本地池，依据的就是与当前的 goroutine 关联的那个 P 的 ID。</p><p>换句话说，一个临时对象池的Put方法或Get方法会获取到哪一个本地池，完全取决于调用它的代码所在的 goroutine 关联的那个 P。</p><h4 id="问题-2：临时对象池是怎样利用内部数据结构来存取值的？"><a href="#问题-2：临时对象池是怎样利用内部数据结构来存取值的？" class="headerlink" title="问题 2：临时对象池是怎样利用内部数据结构来存取值的？"></a>问题 2：临时对象池是怎样利用内部数据结构来存取值的？</h4><p>临时对象池的Put方法总会先试图把新的临时对象，存储到对应的本地池的private字段中，以便在后面获取临时对象的时候，可以快速地拿到一个可用的值。</p><p>只有当这个private字段已经存有某个值时，该方法才会去访问本地池的shared字段。</p><p>相应的，临时对象池的Get方法，总会先试图从对应的本地池的private字段处获取一个临时对象。只有当这个private字段的值为nil时，它才会去访问本地池的shared字段。</p><p>一个本地池的shared字段原则上可以被任何 goroutine 中的代码访问到，不论这个 goroutine 关联的是哪一个 P。这也是我把它叫做共享临时对象列表的原因。</p><p>相比之下，一个本地池的private字段，只可能被与之对应的那个 P 所关联的 goroutine 中的代码访问到，所以可以说，它是 P 级私有的。</p><p>以临时对象池的Put方法为例，它一旦发现对应的本地池的private字段已存有值，就会去访问这个本地池的shared字段。当然，由于shared字段是共享的，所以此时必须受到互斥锁的保护。</p><p>还记得本地池嵌入的那个sync.Mutex类型的字段吗？它就是这里用到的互斥锁，也就是说，本地池本身就拥有互斥锁的功能。Put方法会在互斥锁的保护下，把新的临时对象追加到共享临时对象列表的末尾。</p><p>相应的，临时对象池的Get方法在发现对应本地池的private字段未存有值时，也会去访问后者的shared字段。它会在互斥锁的保护下，试图把该共享临时对象列表中的最后一个元素值取出并作为结果。</p><p>不过，这里的共享临时对象列表也可能是空的，这可能是由于这个本地池中的所有临时对象都已经被取走了，也可能是当前的临时对象池刚被清理过。</p><p>无论原因是什么，Get方法都会去访问当前的临时对象池中的所有本地池，它会去逐个搜索它们的共享临时对象列表。</p><p>只要发现某个共享临时对象列表中包含元素值，它就会把该列表的最后一个元素值取出并作为结果返回。<br><img src="https://static001.geekbang.org/resource/image/df/21/df956fe29f35b41a14f941a9efd80d21.png" alt="从 sync.Pool 中获取临时对象的步骤"></p><p>当然了，即使这样也可能无法拿到一个可用的临时对象，比如，在所有的临时对象池都刚被大清洗的情况下就会是如此。</p><p>这时，Get方法就会使出最后的手段——调用可创建临时对象的那个函数。还记得吗？这个函数是由临时对象池的New字段代表的，并且需要我们在初始化临时对象池的时候给定。如果这个字段的值是nil，那么Get方法此时也只能返回nil了。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/29/go-yu-yan-he-xin-36-jiang-lin-shi-dui-xiang-chi-sync.pool/</id>
    <link href="http://blog.chcaty.cn/2021/11/29/go-yu-yan-he-xin-36-jiang-lin-shi-dui-xiang-chi-sync.pool/"/>
    <published>2021-11-29T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>sync.Pool类型可以被称为临时对象池，它的值可以被用来存储临时的对象。与 Go 语言的很多同步工具一样，sync.Pool类型也属于结构体类型，它的值在被真正使用之后，就不应该再被复制了。</p>
<p>这里的“临时对象”的意思是：不需要持久使用的某一类值。这类值对于程序来说可有可无，但如果有的话会明显更好。它们的创建和销毁可以在任何时候发生，并且完全不会影响到程序的功能。</p>
<p>同时，它们也应该是无需被区分的，其中的任何一个值都可以代替另一个。如果你的某类值完全满足上述条件，那么你就可以把它们存储到临时对象池中。</p>]]>
    </summary>
    <title>Go语言核心36讲-临时对象池sync.Pool</title>
    <updated>2026-04-13T10:24:15.851Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>之前在一些场合下里，我们使用通道的方式看起来都似乎有些蹩脚。</p><p>比如：<strong>声明一个通道，使它的容量与我们手动启用的 goroutine 的数量相同，之后再利用这个通道，让主 goroutine 等待其他 goroutine 的运行结束。</strong></p><p>这一步更具体地说就是：让其他的 goroutine 在运行结束之前，都向这个通道发送一个元素值，并且，让主 goroutine 在最后从这个通道中接收元素值，接收的次数需要与其他的 goroutine 的数量相同。</p><p>这就是下面的coordinateWithChan函数展示的多 goroutine 协作流程。</p><span id="more"></span><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">coordinateWithChan</span><span class="params">()</span></span> &#123;</span><br><span class="line"> sign := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;, <span class="number">2</span>)</span><br><span class="line"> num := <span class="type">int32</span>(<span class="number">0</span>)</span><br><span class="line"> fmt.Printf(<span class="string">&quot;The number: %d [with chan struct&#123;&#125;]\n&quot;</span>, num)</span><br><span class="line"> max := <span class="type">int32</span>(<span class="number">10</span>)</span><br><span class="line"> <span class="keyword">go</span> addNum(&amp;num, <span class="number">1</span>, max, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">  sign &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line"> &#125;)</span><br><span class="line"> <span class="keyword">go</span> addNum(&amp;num, <span class="number">2</span>, max, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">  sign &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line"> &#125;)</span><br><span class="line"> &lt;-sign</span><br><span class="line"> &lt;-sign</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">addNum</span><span class="params">(numP *<span class="type">int32</span>, id, max <span class="type">int32</span>, deferFunc <span class="keyword">func</span>()</span></span>) &#123;</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        deferFunc()</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; ; i++ &#123;</span><br><span class="line">        currNum := atomic.LoadInt32(numP)</span><br><span class="line">        <span class="keyword">if</span> currNum &gt;= max &#123;</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">        newNum := currNum + <span class="number">2</span></span><br><span class="line">        time.Sleep(time.Millisecond * <span class="number">200</span>)</span><br><span class="line">        <span class="keyword">if</span> atomic.CompareAndSwapInt32(numP, currNum, newNum) &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;The number: %d [%d-%d]\n&quot;</span>, newNum, id, i)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;The CAS operation failed. [%d-%d]\n&quot;</span>, id, i)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>addNum函数会把它接受的最后一个参数值作为其中的defer函数。</p><p>我手动启用的两个 goroutine 都会调用addNum函数，而它们传给该函数的最后一个参数值（也就是那个既无参数声明，也无结果声明的函数）都只会做一件事情，那就是向通道sign发送一个元素值。</p><p>看到coordinateWithChan函数中最后的那两行代码了吗？重复的两个接收表达式&lt;-sign，是不是看起来很丑陋？</p><h3 id="前导内容：sync包的WaitGroup类型"><a href="#前导内容：sync包的WaitGroup类型" class="headerlink" title="前导内容：sync包的WaitGroup类型"></a>前导内容：sync包的WaitGroup类型</h3><p>其实，在这种应用场景下，我们可以选用另外一个同步工具，即：sync包的WaitGroup类型。它比通道更加适合实现这种一对多的 goroutine 协作流程。</p><p>sync.WaitGroup类型（以下简称WaitGroup类型）是开箱即用的，也是并发安全的。同时，与我们前面讨论的几个同步工具一样，它一旦被真正使用就不能被复制了。</p><p>WaitGroup类型拥有三个指针方法：Add、Done和Wait。你可以想象该类型中有一个计数器，它的默认值是0。我们可以通过调用该类型值的Add方法来增加，或者减少这个计数器的值。</p><p>一般情况下，我会用这个方法来记录需要等待的 goroutine 的数量。相对应的，这个类型的Done方法，用于对其所属值中计数器的值进行减一操作。我们可以在需要等待的 goroutine 中，通过defer语句调用它。</p><p>而此类型的Wait方法的功能是，阻塞当前的 goroutine，直到其所属值中的计数器归零。如果在该方法被调用的时候，那个计数器的值就是0，那么它将不会做任何事情。</p><p>你可能已经看出来了，WaitGroup类型的值（以下简称WaitGroup值）完全可以被用来替换coordinateWithChan函数中的通道sign。下面的coordinateWithWaitGroup函数就是它的改造版本。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">coordinateWithWaitGroup</span><span class="params">()</span></span> &#123;</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"> wg.Add(<span class="number">2</span>)</span><br><span class="line"> num := <span class="type">int32</span>(<span class="number">0</span>)</span><br><span class="line"> fmt.Printf(<span class="string">&quot;The number: %d [with sync.WaitGroup]\n&quot;</span>, num)</span><br><span class="line"> max := <span class="type">int32</span>(<span class="number">10</span>)</span><br><span class="line"> <span class="keyword">go</span> addNum(&amp;num, <span class="number">3</span>, max, wg.Done)</span><br><span class="line"> <span class="keyword">go</span> addNum(&amp;num, <span class="number">4</span>, max, wg.Done)</span><br><span class="line"> wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很明显，整体代码少了好几行，而且看起来也更加简洁了。这里我先声明了一个WaitGroup类型的变量wg。然后，我调用了它的Add方法并传入了2，因为我会在后面启用两个需要等待的 goroutine。</p><p>由于wg变量的Done方法本身就是一个既无参数声明，也无结果声明的函数，所以我在go语句中调用addNum函数的时候，可以直接把该方法作为最后一个参数值传进去。</p><p>在coordinateWithWaitGroup函数的最后，我调用了wg的Wait方法。如此一来，该函数就可以等到那两个 goroutine 都运行结束之后，再结束执行了。</p><h3 id="今天的问题是：sync-WaitGroup类型值中计数器的值可以小于0吗？"><a href="#今天的问题是：sync-WaitGroup类型值中计数器的值可以小于0吗？" class="headerlink" title="今天的问题是：sync.WaitGroup类型值中计数器的值可以小于0吗？"></a>今天的问题是：sync.WaitGroup类型值中计数器的值可以小于0吗？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>不可以。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>为什么不可以呢，我们解析一下。之所以说WaitGroup值中计数器的值不能小于0，是因为这样会引发一个 panic。 不适当地调用这类值的Done方法和Add方法都会如此。别忘了，我们在调用Add方法的时候是可以传入一个负数的。</p><p>实际上，导致WaitGroup值的方法抛出 panic 的原因不只这一种。</p><p>你需要知道，在我们声明了这样一个变量之后，应该首先根据需要等待的 goroutine，或者其他事件的数量，调用它的Add方法，以使计数器的值大于0。这是确保我们能在后面正常地使用这类值的前提。</p><p>如果我们对它的Add方法的首次调用，与对它的Wait方法的调用是同时发起的，比如，在同时启用的两个 goroutine 中，分别调用这两个方法，<strong>那么就有可能会让这里的Add方法抛出一个 panic。</strong></p><p>这种情况不太容易复现，也正因为如此，我们更应该予以重视。所以，虽然WaitGroup值本身并不需要初始化，但是尽早地增加其计数器的值，还是非常有必要的。</p><p>另外，你可能已经知道，WaitGroup值是可以被复用的，但需要保证其计数周期的完整性。这里的计数周期指的是这样一个过程：该值中的计数器值由0变为了某个正整数，而后又经过一系列的变化，最终由某个正整数又变回了0。</p><p>也就是说，只要计数器的值始于0又归为0，就可以被视为一个计数周期。在一个此类值的生命周期中，它可以经历任意多个计数周期。但是，只有在它走完当前的计数周期之后，才能够开始下一个计数周期。</p><p><img src="https://static001.geekbang.org/resource/image/fa/8d/fac7dfa184053d2a95e121aa17141d8d.png?wh=1350*683" alt="sync.WaitGroup 的计数周期"></p><p>因此，也可以说，如果一个此类值的Wait方法在它的某个计数周期中被调用，那么就会立即阻塞当前的 goroutine，直至这个计数周期完成。在这种情况下，该值的下一个计数周期，必须要等到这个Wait方法执行结束之后，才能够开始。</p><p>如果在一个此类值的Wait方法被执行期间，跨越了两个计数周期，<strong>那么就会引发一个 panic。</strong></p><p>例如，在当前的 goroutine 因调用此类值的Wait方法，而被阻塞的时候，另一个 goroutine 调用了该值的Done方法，并使其计数器的值变为了0。</p><p>这会唤醒当前的 goroutine，并使它试图继续执行Wait方法中其余的代码。但在这时，又有一个 goroutine 调用了它的Add方法，并让其计数器的值又从0变为了某个正整数。<strong>此时，这里的Wait方法就会立即抛出一个 panic。</strong></p><p>纵观上述会引发 panic 的后两种情况，我们可以总结出这样一条关于WaitGroup值的使用禁忌，即：<strong>不要把增加其计数器值的操作和调用其Wait方法的代码，放在不同的 goroutine 中执行。换句话说，要杜绝对同一个WaitGroup值的两种操作的并发执行。</strong></p><p>除了第一种情况外，我们通常需要反复地实验，才能够让WaitGroup值的方法抛出 panic。再次强调，虽然这不是每次都发生，但是在长期运行的程序中，这种情况发生的概率还是不小的，我们必须要重视它们。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题：sync-Once类型值的Do方法是怎么保证只执行参数函数一次的？"><a href="#问题：sync-Once类型值的Do方法是怎么保证只执行参数函数一次的？" class="headerlink" title="问题：sync.Once类型值的Do方法是怎么保证只执行参数函数一次的？"></a>问题：sync.Once类型值的Do方法是怎么保证只执行参数函数一次的？</h4><p>与sync.WaitGroup类型一样，sync.Once类型（以下简称Once类型）也属于结构体类型，同样也是开箱即用和并发安全的。由于这个类型中包含了一个sync.Mutex类型的字段，所以，复制该类型的值也会导致功能的失效。</p><p>Once类型的Do方法只接受一个参数，这个参数的类型必须是func()，即：无参数声明和结果声明的函数。</p><p>该方法的功能并不是对每一种参数函数都只执行一次，而是只执行“首次被调用时传入的”那个函数，并且之后不会再执行任何参数函数。</p><p>所以，如果你有多个只需要执行一次的函数，那么就应该为它们中的每一个都分配一个sync.Once类型的值（以下简称Once值）。</p><p>Once类型中还有一个名叫done的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。不过，该字段的值只可能是0或者1。一旦Do方法的首次调用完成，它的值就会从0变为1。</p><p>你可能会问，既然done字段的值不是0就是1，那为什么还要使用需要四个字节的uint32类型呢？</p><p>原因很简单，因为对它的操作必须是“原子”的。Do方法在一开始就会通过调用atomic.LoadUint32函数来获取该字段的值，并且一旦发现该值为1，就会直接返回。这也初步保证了“Do方法，只会执行首次被调用时传入的函数”。</p><p>不过，单凭这样一个判断的保证是不够的。因为，如果有两个 goroutine 都调用了同一个新的Once值的Do方法，并且几乎同时执行到了其中的这个条件判断代码，那么它们就都会因判断结果为false，而继续执行Do方法中剩余的代码。在</p><p>这个条件判断之后，Do方法会立即锁定其所属值中的那个sync.Mutex类型的字段m。然后，它会在临界区中再次检查done字段的值，并且仅在条件满足时，才会去调用参数函数，以及用原子操作把done的值变为1。</p><p>如果你熟悉 GoF 设计模式中的单例模式的话，那么肯定能看出来，这个Do方法的实现方式，与那个单例模式有很多相似之处。它们都会先在临界区之外，判断一次关键条件，若条件不满足则立即返回。这通常被称为 <strong>“快路径”，或者叫做“快速失败路径”。</strong></p><p>如果条件满足，那么到了临界区中还要再对关键条件进行一次判断，这主要是为了更加严谨。这两次条件判断常被统称为（跨临界区的）“双重检查”。</p><p>由于进入临界区之前，肯定要锁定保护它的互斥锁m，显然会降低代码的执行速度，所以其中的第二次条件判断，以及后续的操作就被称为“慢路径”或者“常规路径”。</p><p>别看Do方法中的代码不多，但它却应用了一个很经典的编程范式。我们在 Go 语言及其标准库中，还能看到不少这个经典范式及它衍生版本的应用案例。</p><p><strong>下面我再来说说这个Do方法在功能方面的两个特点。</strong></p><p><strong>第一个特点</strong>，由于Do方法只会在参数函数执行结束之后把done字段的值变为1，因此，如果参数函数的执行需要很长时间或者根本就不会结束（比如执行一些守护任务），那么就有可能会导致相关 goroutine 的同时阻塞。</p><p>例如，有多个 goroutine 并发地调用了同一个Once值的Do方法，并且传入的函数都会一直执行而不结束。那么，这些 goroutine 就都会因调用了这个Do方法而阻塞。因为，除了那个抢先执行了参数函数的 goroutine 之外，其他的 goroutine 都会被阻塞在锁定该Once值的互斥锁m的那行代码上。</p><p><strong>第二个特点</strong>，Do方法在参数函数执行结束后，对done字段的赋值用的是原子操作，并且，这一操作是被挂在defer语句中的。因此，不论参数函数的执行会以怎样的方式结束，done字段的值都会变为1。</p><p>也就是说，即使这个参数函数没有执行成功（比如引发了一个 panic），我们也无法使用同一个Once值重新执行它了。所以，如果你需要为参数函数的执行设定重试机制，那么就要考虑Once值的适时替换问题。</p><p>在很多时候，我们需要依据Do方法的这两个特点来设计与之相关的流程，以避免不必要的程序阻塞和功能缺失。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/26/go-yu-yan-he-xin-36-jiang-sync.waitgroup-he-sync.once/</id>
    <link href="http://blog.chcaty.cn/2021/11/26/go-yu-yan-he-xin-36-jiang-sync.waitgroup-he-sync.once/"/>
    <published>2021-11-26T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>之前在一些场合下里，我们使用通道的方式看起来都似乎有些蹩脚。</p>
<p>比如：<strong>声明一个通道，使它的容量与我们手动启用的 goroutine 的数量相同，之后再利用这个通道，让主 goroutine 等待其他 goroutine 的运行结束。</strong></p>
<p>这一步更具体地说就是：让其他的 goroutine 在运行结束之前，都向这个通道发送一个元素值，并且，让主 goroutine 在最后从这个通道中接收元素值，接收的次数需要与其他的 goroutine 的数量相同。</p>
<p>这就是下面的coordinateWithChan函数展示的多 goroutine 协作流程。</p>]]>
    </summary>
    <title>Go语言核心36讲-sync.WaitGroup和sync.Once</title>
    <updated>2026-04-13T10:24:15.850Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>Go 语言的另外一种错误处理方式，不过，严格来说，它处理的不是错误，而是一场，并且时一种在我们意料之外的程序异常。</p><span id="more"></span><h3 id="前导知识：运行时恐慌-panic"><a href="#前导知识：运行时恐慌-panic" class="headerlink" title="前导知识：运行时恐慌 panic"></a>前导知识：运行时恐慌 panic</h3><p>这种程序异常被叫做 panic，我把它翻译为运行时恐慌。其中的“恐慌”二字是由 panic 直译过来的，而之所以前面又加上了“运行时”三个字，是因为这种异常只会在程序运行的时候被抛出来。</p><p>我们举个具体的例子来看看。</p><p>比如说，一个 Go 程序里有一个切片，它的长度是 5，也就是说该切片中的元素值的索引分别为0、1、2、3、4，但是，我在程序里却想通过索引5访问其中的元素值，显而易见，这样的访问是不正确的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  s1 := []<span class="type">int</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>&#125;</span><br><span class="line">  e5 := s1[<span class="number">5</span>]</span><br><span class="line">  _ = e5</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Go 程序，确切地说是程序内嵌的 Go 语言运行时系统，会在执行到这行代码的时候抛出一个“index out of range”的 panic，用以提示你索引越界了。</p><p>当然了，这不仅仅是个提示。当 panic 被抛出之后，如果我们没有在程序里添加任何保护措施的话，程序（或者说代表它的那个进程）就会在打印出 panic 的详细情况（以下简称 panic 详情）之后，终止运行。</p><p>现在，就让我们来看一下这样的 panic 详情中都有什么。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">panic</span>: runtime <span class="type">error</span>: index out of <span class="keyword">range</span></span><br><span class="line"></span><br><span class="line">goroutine <span class="number">1</span> [running]:</span><br><span class="line">main.main()</span><br><span class="line"> /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q0/demo47.<span class="keyword">go</span>:<span class="number">5</span> +<span class="number">0x3d</span></span><br><span class="line">exit status <span class="number">2</span></span><br></pre></td></tr></table></figure><p>这份详情的第一行是“panic: runtime error: index out of range”。其中的“runtime error”的含义是，这是一个runtime代码包中抛出的 panic。在这个 panic 中，包含了一个runtime.Error接口类型的值。runtime.Error接口内嵌了error接口，并做了一点点扩展，runtime包中有不少它的实现类型。</p><p>实际上，此详情中的“panic：”右边的内容，正是这个 panic 包含的runtime.Error类型值的字符串表示形式。</p><p>此外，panic 详情中，一般还会包含与它的引发原因有关的 goroutine 的代码执行信息。正如前述详情中的“goroutine 1 [running]”，它表示有一个 ID 为1的 goroutine 在此 panic 被引发的时候正在运行。</p><p>注意，这里的 ID 其实并不重要，因为它只是 Go 语言运行时系统内部给予的一个 goroutine 编号，我们在程序中是无法获取和更改的。</p><p>我们再看下一行，“main.main()”表明了这个 goroutine 包装的go函数就是命令源码文件中的那个main函数，也就是说这里的 goroutine 正是主 goroutine。再下面的一行，指出的就是这个 goroutine 中的哪一行代码在此 panic 被引发时正在执行。</p><p>这包含了此行代码在其所属的源码文件中的行数，以及这个源码文件的绝对路径。这一行最后的+0x3d代表的是：此行代码相对于其所属函数的入口程序计数偏移量。不过，一般情况下它的用处并不大。</p><p>最后，“exit status 2”表明我的这个程序是以退出状态码2结束运行的。在大多数操作系统中，只要退出状态码不是0，都意味着程序运行的非正常结束。在 Go 语言中，因 panic 导致程序结束运行的退出状态码一般都会是2。</p><p>综上所述，我们从上边的这个 panic 详情可以看出，作为此 panic 的引发根源的代码处于代码文件中的第 5 行，同时被包含在main包（也就是命令源码文件所在的代码包）的main函数中。</p><h3 id="今天的问题是：从-panic-被引发到程序终止运行的大致过程是什么？"><a href="#今天的问题是：从-panic-被引发到程序终止运行的大致过程是什么？" class="headerlink" title="今天的问题是：从 panic 被引发到程序终止运行的大致过程是什么？"></a>今天的问题是：从 panic 被引发到程序终止运行的大致过程是什么？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>我们先说一个大致的过程：某个函数中的某行代码有意或无意地引发了一个 panic。这时，初始的 panic 详情会被建立起来，并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上，也就是调用栈中的上一级。</p><p>这也意味着，此行代码所属函数的执行随即终止。紧接着，控制权并不会在此有片刻的停留，它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端，也就是我们编写的最外层函数那里。</p><p>这里的最外层函数指的是go函数，对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里，而是被 Go 语言运行时系统收回。</p><p>随后，程序崩溃并终止运行，承载程序这次运行的进程也会随之死亡并消失。与此同时，在这个控制权传播的过程中，panic 详情会被逐渐地积累和完善，并会在程序终止之前被打印出来。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>panic 可能是我们在无意间（或者说一不小心）引发的，如前文所述的索引越界。这类 panic 是真正的、在我们意料之外的程序异常。不过，除此之外，我们还是可以有意地引发 panic。</p><p>Go 语言的内建函数panic是专门用于引发 panic 的。panic函数使程序开发者可以在程序运行期间报告异常。</p><p>注意，这与从函数返回错误值的意义是完全不同的。当我们的函数返回一个非nil的错误值时，函数的调用方有权选择不处理，并且不处理的后果往往是不致命的。</p><p>这里的“不致命”的意思是，不至于使程序无法提供任何功能（也可以说僵死）或者直接崩溃并终止运行（也就是真死）。</p><p>但是，当一个 panic 发生时，如果我们不施加任何保护措施，那么导致的直接后果就是程序崩溃，就像前面描述的那样，这显然是致命的。</p><p>代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Enter function main.&quot;</span>)</span><br><span class="line">  caller1()</span><br><span class="line">  fmt.Println(<span class="string">&quot;Exit function main.&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">caller1</span><span class="params">()</span></span> &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Enter function caller1.&quot;</span>)</span><br><span class="line">  caller2()</span><br><span class="line">  fmt.Println(<span class="string">&quot;Exit function caller1.&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">caller2</span><span class="params">()</span></span> &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Enter function caller2.&quot;</span>)</span><br><span class="line">  s1 := []<span class="type">int</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>&#125;</span><br><span class="line">  e5 := s1[<span class="number">5</span>]</span><br><span class="line">  _ = e5</span><br><span class="line">  fmt.Println(<span class="string">&quot;Exit function caller2.&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>panic 详情会在控制权传播的过程中，被逐渐地积累和完善，并且，控制权会一级一级地沿着调用栈的反方向传播至顶端。</p><p>因此，在针对某个 goroutine 的代码执行信息中，调用栈底端的信息会先出现，然后是上一级调用的信息，以此类推，最后才是此调用栈顶端的信息。</p><p>比如，main函数调用了caller1函数，而caller1函数又调用了caller2函数，那么caller2函数中代码的执行信息会先出现，然后是caller1函数中代码的执行信息，最后才是main函数的信息。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">goroutine <span class="number">1</span> [running]:</span><br><span class="line">main.caller2()</span><br><span class="line"> /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.<span class="keyword">go</span>:<span class="number">22</span> +<span class="number">0x91</span></span><br><span class="line">main.caller1()</span><br><span class="line"> /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.<span class="keyword">go</span>:<span class="number">15</span> +<span class="number">0x66</span></span><br><span class="line">main.main()</span><br><span class="line"> /Users/haolin/GeekTime/Golang_Puzzlers/src/puzzlers/article19/q1/demo48.<span class="keyword">go</span>:<span class="number">9</span> +<span class="number">0x66</span></span><br><span class="line">exit status <span class="number">2</span></span><br></pre></td></tr></table></figure><p><img src="https://static001.geekbang.org/resource/image/60/d7/606ff433a6b58510f215e57792822bd7.png" alt="从 panic 到程序崩溃"></p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/25/go-yu-yan-he-xin-36-jiang-panic-han-shu-recover-han-shu-yi-ji-defer-yu-ju-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/25/go-yu-yan-he-xin-36-jiang-panic-han-shu-recover-han-shu-yi-ji-defer-yu-ju-shang/"/>
    <published>2021-11-25T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>Go 语言的另外一种错误处理方式，不过，严格来说，它处理的不是错误，而是一场，并且时一种在我们意料之外的程序异常。</p>]]>
    </summary>
    <title>Go语言核心36讲-panic函数、recover函数以及defer语句（上）</title>
    <updated>2026-04-13T10:24:15.850Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>error类型其实是一个接口类型，也是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。Error方法不接受任何参数，但是会返回一个string类型的结果。它的作用是返回错误信息的字符串表示形式。</p><span id="more"></span><p>我们使用error类型的方式通常是，在函数声明的结果列表的最后，声明一个该类型的结果，同时在调用这个函数之后，先判断它返回的最后一个结果值是否“不为nil”。</p><p>如果这个值“不为nil”，那么就进入错误处理流程，否则就继续进行正常的流程。下面是一个例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;errors&quot;</span></span><br><span class="line">  <span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">echo</span><span class="params">(request <span class="type">string</span>)</span></span> (response <span class="type">string</span>, err <span class="type">error</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> request == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">    err = errors.New(<span class="string">&quot;empty request&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  response = fmt.Sprintf(<span class="string">&quot;echo: %s&quot;</span>, request)</span><br><span class="line">  <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">for</span> _, req := <span class="keyword">range</span> []<span class="type">string</span>&#123;<span class="string">&quot;&quot;</span>, <span class="string">&quot;hello!&quot;</span>&#125; &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;request: %s\n&quot;</span>, req)</span><br><span class="line">    resp, err := echo(req)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">      fmt.Printf(<span class="string">&quot;error: %s\n&quot;</span>, err)</span><br><span class="line">      <span class="keyword">continue</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;response: %s\n&quot;</span>, resp)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们先看echo函数的声明。echo函数接受一个string类型的参数request，并会返回两个结果。</p><p>这两个结果都是有名称的，第一个结果response也是string类型的，它代表了这个函数正常执行后的结果值。</p><p>第二个结果err就是error类型的，它代表了函数执行出错时的结果值，同时也包含了具体的错误信息。</p><p>当echo函数被调用时，它会先检查参数request的值。如果该值为空字符串，那么它就会通过调用errors.New函数，为结果err赋值，然后忽略掉后边的操作并直接返回。</p><p>此时，结果response的值也会是一个空字符串。如果request的值并不是空字符串，那么它就为结果response赋一个适当的值，然后返回，此时结果err的值会是nil。</p><p>再来看main函数中的代码。我在每次调用echo函数之后，都会把它返回的结果值赋给变量resp和err，并且总是先检查err的值是否“不为nil”，如果是，就打印错误信息，否则就打印常规的响应信息。</p><p>这里值得注意的地方有两个。第一，在echo函数和main函数中，我都使用到了卫述语句。我在前面讲函数用法的时候也提到过卫述语句。简单地讲，它就是被用来检查后续操作的前置条件并进行相应处理的语句。</p><p>对于echo函数来说，它进行常规操作的前提是：传入的参数值一定要符合要求。而对于调用echo函数的程序来说，进行后续操作的前提就是echo函数的执行不能出错。</p><blockquote><p>我们在进行错误处理的时候经常会用到卫述语句，以至于有些人会吐槽说：“我的程序满屏都是卫述语句，简直是太难看了！”<br>不过，我倒认为这有可能是程序设计上的问题。每个编程语言的理念和风格几乎都会有明显的不同，我们常常需要顺应它们的纹理去做设计，而不是用其他语言的编程思想来编写当下语言的程序。</p></blockquote><p>errors.New函数，这是一种最基本的生成错误值的方式。我们调用它的时候传入一个由字符串代表的错误信息，它会给返回给我们一个包含了这个错误信息的error类型值。该值的静态类型当然是error，而动态类型则是一个在errors包中的，包级私有的类型*errorString。</p><p>显然，errorString类型拥有的一个指针方法实现了error接口中的Error方法。这个方法在被调用后，会原封不动地返回我们之前传入的错误信息。实际上，error类型值的Error方法就相当于其他类型值的String方法。</p><p>我们已经知道，通过调用fmt.Printf函数，并给定占位符%s就可以打印出某个值的字符串表示形式。</p><p>对于其他类型的值来说，只要我们能为这个类型编写一个String方法，就可以自定义它的字符串表示形式。而对于error类型值，它的字符串表示形式则取决于它的Error方法。</p><p>在上述情况下，fmt.Printf函数如果发现被打印的值是一个error类型的值，那么就会去调用它的Error方法。fmt包中的这类打印函数其实都是这么做的。</p><p>顺便提一句，当我们想通过模板化的方式生成错误信息，并得到错误值时，可以使用fmt.Errorf函数。该函数所做的其实就是先调用fmt.Sprintf函数，得到确切的错误信息；再调用errors.New函数，得到包含该错误信息的error类型值，最后返回该值。</p><h3 id="今天的问题是：对于具体错误的判断，Go-语言中都有哪些惯用法？"><a href="#今天的问题是：对于具体错误的判断，Go-语言中都有哪些惯用法？" class="headerlink" title="今天的问题是：对于具体错误的判断，Go 语言中都有哪些惯用法？"></a>今天的问题是：对于具体错误的判断，Go 语言中都有哪些惯用法？</h3><p>由于error是一个接口类型，所以即使同为error类型的错误值，它们的实际类型也可能不同。这个问题还可以换一种问法，即：怎样判断一个错误值具体代表的是哪一类错误？</p><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><ol><li>对于类型在已知范围内的一系列错误值，一般使用类型断言表达式或类型switch语句来判断；</li><li>对于已有相应变量且类型相同的一系列错误值，一般直接使用判等操作来判断；</li><li>对于没有相应变量且类型未知的一系列错误值，只能使用其错误信息的字符串表示形式来做判断。</li></ol><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>类型在已知范围内的错误值其实是最容易分辨的。就拿os包中的几个代表错误的类型os.PathError、os.LinkError、os.SyscallError和os&#x2F;exec.Error来说，它们的指针类型都是error接口的实现类型，同时它们也都包含了一个名叫Err，类型为error接口类型的代表潜在错误的字段。</p><p>如果我们得到一个error类型值，并且知道该值的实际类型肯定是它们中的某一个，那么就可以用类型switch语句去做判断。例如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">underlyingError</span><span class="params">(err <span class="type">error</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">  <span class="keyword">switch</span> err := err.(<span class="keyword">type</span>) &#123;</span><br><span class="line">  <span class="keyword">case</span> *os.PathError:</span><br><span class="line">    <span class="keyword">return</span> err.Err</span><br><span class="line">  <span class="keyword">case</span> *os.LinkError:</span><br><span class="line">    <span class="keyword">return</span> err.Err</span><br><span class="line">  <span class="keyword">case</span> *os.SyscallError:</span><br><span class="line">    <span class="keyword">return</span> err.Err</span><br><span class="line">  <span class="keyword">case</span> *exec.Error:</span><br><span class="line">    <span class="keyword">return</span> err.Err</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>函数underlyingError的作用是：获取和返回已知的操作系统相关错误的潜在错误值。其中的类型switch语句中有若干个case子句，分别对应了上述几个错误类型。当它们被选中时，都会把函数参数err的Err字段作为结果值返回。如果它们都未被选中，那么该函数就会直接把参数值作为结果返回，即放弃获取潜在错误值。</p><p>只要类型不同，我们就可以如此分辨。但是在错误值类型相同的情况下，这些手段就无能为力了。在 Go 语言的标准库中也有不少以相同方式创建的同类型的错误值。</p><p>我们还拿os包来说，其中不少的错误值都是通过调用errors.New函数来初始化的，比如：os.ErrClosed、os.ErrInvalid以及os.ErrPermission，等等。</p><p>注意，与前面讲到的那些错误类型不同，这几个都是已经定义好的、确切的错误值。os包中的代码有时候会把它们当做潜在错误值，封装进前面那些错误类型的值中。</p><p>如果我们在操作文件系统的时候得到了一个错误值，并且知道该值的潜在错误值肯定是上述值中的某一个，那么就可以用普通的switch语句去做判断，当然了，用if语句和判等操作符也是可以的。例如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">printError := <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>, err <span class="type">error</span>)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;nil error&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line">  err = underlyingError(err)</span><br><span class="line">  <span class="keyword">switch</span> err &#123;</span><br><span class="line">  <span class="keyword">case</span> os.ErrClosed:</span><br><span class="line">    fmt.Printf(<span class="string">&quot;error(closed)[%d]: %s\n&quot;</span>, i, err)</span><br><span class="line">  <span class="keyword">case</span> os.ErrInvalid:</span><br><span class="line">    fmt.Printf(<span class="string">&quot;error(invalid)[%d]: %s\n&quot;</span>, i, err)</span><br><span class="line">  <span class="keyword">case</span> os.ErrPermission:</span><br><span class="line">    fmt.Printf(<span class="string">&quot;error(permission)[%d]: %s\n&quot;</span>, i, err)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个由printError变量代表的函数会接受一个error类型的参数值。该值总会代表某个文件操作相关的错误，这是我故意地以不正确的方式操作文件后得到的。</p><p>虽然我不知道这些错误值的类型的范围，但却知道它们或它们的潜在错误值一定是某个已经在os包中定义的值。</p><p>所以，我先用underlyingError函数得到它们的潜在错误值，当然也可能只得到原错误值而已。然后，我用switch语句对错误值进行判等操作，三个case子句分别对应我刚刚提到的那三个已存在于os包中的错误值。如此一来，我就能分辨出具体错误了。</p><p>对于上面这两种情况，我们都有明确的方式去解决。但是，如果我们对一个错误值可能代表的含义知之甚少，那么就只能通过它拥有的错误信息去做判断了。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/25/go-yu-yan-he-xin-36-jiang-cuo-wu-chu-li-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/25/go-yu-yan-he-xin-36-jiang-cuo-wu-chu-li-shang/"/>
    <published>2021-11-25T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>error类型其实是一个接口类型，也是一个 Go 语言的内建类型。在这个接口类型的声明中只包含了一个方法Error。Error方法不接受任何参数，但是会返回一个string类型的结果。它的作用是返回错误信息的字符串表示形式。</p>]]>
    </summary>
    <title>Go语言核心36讲-错误处理（上）</title>
    <updated>2026-04-13T10:24:15.856Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>互斥锁是一个很有用的同步工具，它可以保证每一时刻进入临界区的 goroutine 只有一个。读写锁对共享资源的写操作和读操作则区别看待，并消除了读操作之间的互斥。</p><p>条件变量主要是用于协调想要访问共享资源的那些线程。当共享资源的状态发生变化时，它可以被用来通知被互斥锁阻塞的线程，它既可以基于互斥锁，也可以基于读写锁。当然了，读写锁也是一种互斥锁，前者是对后者的扩展。</p><p>通过对互斥锁的合理使用，我们可以使一个 goroutine 在执行临界区中的代码时，不被其他的 goroutine 打扰。不过，虽然不会被打扰，但是它仍然可能会被中断（interruption）。</p><span id="more"></span><h3 id="前导内容：原子性执行与原子操作"><a href="#前导内容：原子性执行与原子操作" class="headerlink" title="前导内容：原子性执行与原子操作"></a>前导内容：原子性执行与原子操作</h3><p>我们已经知道，对于一个 Go 程序来说，Go 语言运行时系统中的调度器会恰当地安排其中所有的 goroutine 的运行。不过，在同一时刻，只可能有少数的 goroutine 真正地处于运行状态，并且这个数量只会与 M 的数量一致，而不会随着 G 的增多而增长。</p><p>所以，为了公平起见，调度器总是会频繁地换上或换下这些 goroutine。<strong>换上</strong> 的意思是，让一个 goroutine 由非运行状态转为运行状态，并促使其中的代码在某个 CPU 核心上执行。</p><p><strong>换下</strong> 的意思正好相反，即：使一个 goroutine 中的代码中断执行，并让它由运行状态转为非运行状态。</p><p>这个中断的时机有很多，任何两条语句执行的间隙，甚至在某条语句执行的过程中都是可以的。</p><p>即使这些语句在临界区之内也是如此。所以，我们说，互斥锁虽然可以保证临界区中代码的串行执行，但却不能保证这些代码执行的原子性（atomicity）。</p><p>在众多的同步工具中，真正能够保证原子性执行的只有原子操作（atomic operation）。原子操作在进行的过程中是不允许中断的。在底层，这会由 CPU 提供芯片级别的支持，所以绝对有效。即使在拥有多 CPU 核心，或者多 CPU 的计算机系统中，原子操作的保证也是不可撼动的。</p><p>这使得原子操作可以完全地消除竞态条件，并能够绝对地保证并发安全性。并且，它的执行速度要比其他的同步工具快得多，通常会高出好几个数量级。不过，它的缺点也很明显。</p><p><strong>更具体地说，正是因为原子操作不能被中断，所以它需要足够简单，并且要求快速。</strong></p><p>你可以想象一下，如果原子操作迟迟不能完成，而它又不会被中断，那么将会给计算机执行指令的效率带来多么大的影响。因此，操作系统层面只对针对二进制位或整数的原子操作提供了支持。</p><p>Go 语言的原子操作当然是基于 CPU 和操作系统的，所以它也只针对少数数据类型的值提供了原子操作函数。这些函数都存在于标准库代码包sync&#x2F;atomic中。</p><h3 id="今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"><a href="#今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？" class="headerlink" title="今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"></a>今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>sync&#x2F;atomic包中的函数可以做的原子操作有：加法（add）、比较并交换（compare and swap，简称 CAS）、加载（load）、存储（store）和交换（swap）。</p><p>这些函数针对的数据类型并不多。但是，对这些类型中的每一个，sync&#x2F;atomic包都会有一套函数给予支持。这些数据类型有：int32、int64、uint32、uint64、uintptr，以及unsafe包中的Pointer。不过，针对unsafe.Pointer类型，该包并未提供进行原子加法操作的函数。</p><p>此外，sync&#x2F;atomic包还提供了一个名为Value的类型，它可以被用来存储任意类型的值。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p><strong>第一个衍生问题 ：</strong> 我们都知道，传入这些原子操作函数的第一个参数值对应的都应该是那个被操作的值。比如，atomic.AddInt32函数的第一个参数，对应的一定是那个要被增大的整数。可是，这个参数的类型为什么不是int32而是*int32呢？</p><p>回答是：因为原子操作函数需要的是被操作值的指针，而不是这个值本身；被传入函数的参数值都会被复制，像这种基本类型的值一旦被传入函数，就已经与函数外的那个值毫无关系了。</p><p>所以，传入值本身没有任何意义。unsafe.Pointer类型虽然是指针类型，但是那些原子操作函数要操作的是这个指针值，而不是它指向的那个值，所以需要的仍然是指向这个指针值的指针。</p><p>只要原子操作函数拿到了被操作值的指针，就可以定位到存储该值的内存地址。只有这样，它们才能够通过底层的指令，准确地操作这个内存地址上的数据。</p><p><strong>第二个衍生问题：</strong> 用于原子加法操作的函数可以做原子减法吗？比如，atomic.AddInt32函数可以用于减小那个被操作的整数值吗？</p><p>回答是：当然是可以的。atomic.AddInt32函数的第二个参数代表差量，它的类型是int32，是有符号的。如果我们想做原子减法，那么把这个差量设置为负整数就可以了。</p><p>对于atomic.AddInt64函数来说也是类似的。不过，要想用atomic.AddUint32和atomic.AddUint64函数做原子减法，就不能这么直接了，因为它们的第二个参数的类型分别是uint32和uint64，都是无符号的，不过，这也是可以做到的，就是稍微麻烦一些。</p><p>例如，如果想对uint32类型的被操作值18做原子减法，比如说差量是-3，那么我们可以先把这个差量转换为有符号的int32类型的值，然后再把该值的类型转换为uint32，用表达式来描述就是uint32(int32(-3))。</p><p>不过要注意，直接这样写会使 Go 语言的编译器报错，它会告诉你：“常量-3不在uint32类型可表示的范围内”，换句话说，这样做会让表达式的结果值溢出。</p><p>不过，如果我们先把int32(-3)的结果值赋给变量delta，再把delta的值转换为uint32类型的值，就可以绕过编译器的检查并得到正确的结果了。</p><p>最后，我们把这个结果作为atomic.AddUint32函数的第二个参数值，就可以达到对uint32类型的值做原子减法的目的了。</p><p>还有一种更加直接的方式。我们可以依据下面这个表达式来给定atomic.AddUint32函数的第二个参数值：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">^<span class="type">uint32</span>(-N<span class="number">-1</span>))</span><br></pre></td></tr></table></figure><p>其中的N代表由负整数表示的差量。也就是说，我们先要把差量的绝对值减去1，然后再把得到的这个无类型的整数常量，转换为uint32类型的值，最后，在这个值之上做按位异或操作，就可以获得最终的参数值了。</p><p>这么做的原理也并不复杂。简单来说，此表达式的结果值的补码，与使用前一种方法得到的值的补码相同，所以这两种方式是等价的。我们都知道，整数在计算机中是以补码的形式存在的，所以在这里，结果值的补码相同就意味着表达式的等价。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-yuan-zi-cao-zuo-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-yuan-zi-cao-zuo-shang/"/>
    <published>2021-11-24T15:45:58.000Z</published>
    <summary>
      <![CDATA[<p>互斥锁是一个很有用的同步工具，它可以保证每一时刻进入临界区的 goroutine 只有一个。读写锁对共享资源的写操作和读操作则区别看待，并消除了读操作之间的互斥。</p>
<p>条件变量主要是用于协调想要访问共享资源的那些线程。当共享资源的状态发生变化时，它可以被用来通知被互斥锁阻塞的线程，它既可以基于互斥锁，也可以基于读写锁。当然了，读写锁也是一种互斥锁，前者是对后者的扩展。</p>
<p>通过对互斥锁的合理使用，我们可以使一个 goroutine 在执行临界区中的代码时，不被其他的 goroutine 打扰。不过，虽然不会被打扰，但是它仍然可能会被中断（interruption）。</p>]]>
    </summary>
    <title>Go语言核心36讲-原子操作（上）</title>
    <updated>2026-04-13T10:24:15.851Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"><a href="#今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？" class="headerlink" title="今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"></a>今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？</h3><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p><strong>第三个衍生问题</strong>： 比较并交换操作与交换操作相比有什么不同？优势在哪里？</p><p>回答是：比较并交换操作即 CAS 操作，是有条件的交换操作，只有在条件满足的情况下才会进行值的交换。</p><span id="more"></span><p>所谓的交换指的是，把新值赋给变量，并返回变量的旧值。</p><p>在进行 CAS 操作的时候，函数会先判断被操作变量的当前值，是否与我们预期的旧值相等。如果相等，它就把新值赋给该变量，并返回true以表明交换操作已进行；否则就忽略交换操作，并返回false。</p><p>可以看到，CAS 操作并不是单一的操作，而是一种操作组合。这与其他的原子操作都不同。正因为如此，它的用途要更广泛一些。例如，我们将它与for语句联用就可以实现一种简易的自旋锁（spinlock）。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line"> <span class="keyword">if</span> atomic.CompareAndSwapInt32(&amp;num2, <span class="number">10</span>, <span class="number">0</span>) &#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;The second number has gone to zero.&quot;</span>)</span><br><span class="line">  <span class="keyword">break</span></span><br><span class="line"> &#125;</span><br><span class="line"> time.Sleep(time.Millisecond * <span class="number">500</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在for语句中的 CAS 操作可以不停地检查某个需要满足的条件，一旦条件满足就退出for循环。这就相当于，只要条件未被满足，当前的流程就会被一直“阻塞”在这里。</p><p>这在效果上与互斥锁有些类似。不过，它们的适用场景是不同的。我们在使用互斥锁的时候，总是假设共享资源的状态会被其他的 goroutine 频繁地改变。</p><p>而for语句加 CAS 操作的假设往往是：共享资源状态的改变并不频繁，或者，它的状态总会变成期望的那样。这是一种更加乐观，或者说更加宽松的做法。</p><p><strong>第四个衍生问题：</strong> 假设我已经保证了对一个变量的写操作都是原子操作，比如：加或减、存储、交换等等，那我对它进行读操作的时候，还有必要使用原子操作吗？</p><p>回答是：很有必要。其中的道理你可以对照一下读写锁。为什么在读写锁保护下的写操作和读操作之间是互斥的？这是为了防止读操作读到没有被修改完的值，对吗？</p><p>如果写操作还没有进行完，读操作就来读了，那么就只能读到仅修改了一部分的值。这显然破坏了值的完整性，读出来的值也是完全错误的。</p><p>所以，一旦你决定了要对一个共享资源进行保护，那就要做到完全的保护。不完全的保护基本上与不保护没有什么区别。</p><p>好了，上面的主问题以及相关的衍生问题涉及了原子操作函数的用法、原理、对比和一些最佳实践，希望你已经理解了。</p><p>由于这里的原子操作函数只支持非常有限的数据类型，所以在很多应用场景下，互斥锁往往是更加适合的。</p><p>不过，一旦我们确定了在某个场景下可以使用原子操作函数，比如：只涉及并发地读写单一的整数类型值，或者多个互不相关的整数类型值，那就不要再考虑互斥锁了。</p><p>这主要是因为原子操作函数的执行速度要比互斥锁快得多。而且，它们使用起来更加简单，不会涉及临界区的选择，以及死锁等问题。当然了，在使用 CAS 操作的时候，我们还是要多加注意的，因为它可以被用来模仿锁，并有可能“阻塞”流程。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题：怎样用好sync-atomic-Value？"><a href="#问题：怎样用好sync-atomic-Value？" class="headerlink" title="问题：怎样用好sync&#x2F;atomic.Value？"></a>问题：怎样用好sync&#x2F;atomic.Value？</h4><p>为了扩大原子操作的适用范围，Go 语言在 1.4 版本发布的时候向sync&#x2F;atomic包中添加了一个新的类型Value。此类型的值相当于一个容器，可以被用来“原子地”存储和加载任意的值。</p><p>atomic.Value类型是开箱即用的，我们声明一个该类型的变量（以下简称原子变量）之后就可以直接使用了。这个类型使用起来很简单，它只有两个指针方法：Store和Load。不过，虽然简单，但还是有一些值得注意的地方的。</p><p>首先一点，一旦atomic.Value类型的值（以下简称原子值）被真正使用，它就不应该再被复制了。什么叫做“真正使用”呢？</p><p>我们只要用它来存储值了，就相当于开始真正使用了。atomic.Value类型属于结构体类型，而结构体类型属于值类型。</p><p>所以，复制该类型的值会产生一个完全分离的新值。这个新值相当于被复制的那个值的一个快照。之后，不论后者存储的值怎样改变，都不会影响到前者，反之亦然。</p><p>另外，关于用原子值来存储值，有两条强制性的使用规则。<strong>第一条规则，不能用原子值存储nil。</strong></p><p>也就是说，我们不能把nil作为参数值传入原子值的Store方法，否则就会引发一个 panic。</p><p>这里要注意，如果有一个接口类型的变量，它的动态值是nil，但动态类型却不是nil，那么它的值就不等于nil。我在前面讲接口的时候和你说明过这个问题。正因为如此，这样一个变量的值是可以被存入原子值的。</p><p><strong>第二条规则，我们向原子值存储的第一个值，决定了它今后能且只能存储哪一个类型的值。</strong></p><p>例如，我第一次向一个原子值存储了一个string类型的值，那我在后面就只能用该原子值来存储字符串了。如果我又想用它存储结构体，那么在调用它的Store方法的时候就会引发一个 panic。这个 panic 会告诉我，这次存储的值的类型与之前的不一致。</p><p>你可能会想：我先存储一个接口类型的值，然后再存储这个接口的某个实现类型的值，这样是不是可以呢？</p><p>很可惜，这样是不可以的，同样会引发一个 panic。因为原子值内部是依据被存储值的实际类型来做判断的。所以，即使是实现了同一个接口的不同类型，它们的值也不能被先后存储到同一个原子值中。</p><p>遗憾的是，我们无法通过某个方法获知一个原子值是否已经被真正使用，并且，也没有办法通过常规的途径得到一个原子值可以存储值的实际类型。这使得我们误用原子值的可能性大大增加，尤其是在多个地方使用同一个原子值的时候。</p><h4 id="几条具体的使用建议"><a href="#几条具体的使用建议" class="headerlink" title="几条具体的使用建议"></a>几条具体的使用建议</h4><ol><li>不要把内部使用的原子值暴露给外界。比如，声明一个全局的原子变量并不是一个正确的做法。这个变量的访问权限最起码也应该是包级私有的。</li><li>如果不得不让包外，或模块外的代码使用你的原子值，那么可以声明一个包级私有的原子变量，然后再通过一个或多个公开的函数，让外界间接地使用到它。注意，这种情况下不要把原子值传递到外界，不论是传递原子值本身还是它的指针值。</li><li>如果通过某个函数可以向内部的原子值存储值的话，那么就应该在这个函数中先判断被存储值类型的合法性。若不合法，则应该直接返回对应的错误值，从而避免 panic 的发生。</li><li>如果可能的话，我们可以把原子值封装到一个数据类型中，比如一个结构体类型。这样，我们既可以通过该类型的方法更加安全地存储值，又可以在该类型中包含可存储值的合法类型信息。</li></ol><p>除了上述使用建议之外，我还要再特别强调一点：尽量不要向原子值中存储引用类型的值。因为这很容易造成安全漏洞。请看下面的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> box6 atomic.Value</span><br><span class="line">v6 := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">box6.Store(v6)</span><br><span class="line">v6[<span class="number">1</span>] = <span class="number">4</span> <span class="comment">// 注意，此处的操作不是并发安全的！</span></span><br></pre></td></tr></table></figure><p>我把一个[]int类型的切片值v6, 存入了原子值box6。注意，切片类型属于引用类型。所以，我在外面改动这个切片值，就等于修改了box6中存储的那个值。这相当于绕过了原子值而进行了非并发安全的操作。那么，应该怎样修补这个漏洞呢？可以这样做：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">store := <span class="function"><span class="keyword">func</span><span class="params">(v []<span class="type">int</span>)</span></span> &#123;</span><br><span class="line"> replica := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="built_in">len</span>(v))</span><br><span class="line"> <span class="built_in">copy</span>(replica, v)</span><br><span class="line"> box6.Store(replica)</span><br><span class="line">&#125;</span><br><span class="line">store(v6)</span><br><span class="line">v6[<span class="number">2</span>] = <span class="number">5</span> <span class="comment">// 此处的操作是安全的。</span></span><br></pre></td></tr></table></figure><p>我先为切片值v6创建了一个完全的副本。这个副本涉及的数据已经与原值毫不相干了。然后，我再把这个副本存入box6。如此一来，无论我再对v6的值做怎样的修改，都不会破坏box6提供的安全保护。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-yuan-zi-cao-zuo-xia/</id>
    <link href="http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-yuan-zi-cao-zuo-xia/"/>
    <published>2021-11-24T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"><a href="#今天的问题是：sync-atomic包中提供了几种原子操作？可操作的数据类型又有哪些？" class="headerlink" title="今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？"></a>今天的问题是：sync&#x2F;atomic包中提供了几种原子操作？可操作的数据类型又有哪些？</h3><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p><strong>第三个衍生问题</strong>： 比较并交换操作与交换操作相比有什么不同？优势在哪里？</p>
<p>回答是：比较并交换操作即 CAS 操作，是有条件的交换操作，只有在条件满足的情况下才会进行值的交换。</p>]]>
    </summary>
    <title>Go语言核心36讲-原子操作（下）</title>
    <updated>2026-04-13T10:24:15.851Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="前导知识：条件变量与互斥锁"><a href="#前导知识：条件变量与互斥锁" class="headerlink" title="前导知识：条件变量与互斥锁"></a>前导知识：条件变量与互斥锁</h3><p>我们常常会把条件变量这个同步工具拿来与互斥锁一起讨论。实际上，条件变量是基于互斥锁的，它必须有互斥锁的支撑才能发挥作用。</p><p>条件变量并不是被用来保护临界区和共享资源的，它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时，它可以被用来通知被互斥锁阻塞的线程。</p><span id="more"></span><p>比如说，我们两个人在共同执行一项秘密任务，这需要在不直接联系和见面的前提下进行。我需要向一个信箱里放置情报，你需要从这个信箱中获取情报。这个信箱就相当于一个共享资源，而我们就分别是进行写操作的线程和进行读操作的线程。</p><p>如果我在放置的时候发现信箱里还有未被取走的情报，那就不再放置，而先返回。另一方面，如果你在获取的时候发现信箱里没有情报，那也只能先回去了。这就相当于写的线程或读的线程阻塞的情况。</p><p>虽然我们俩都有信箱的钥匙，但是同一时刻只能有一个人插入钥匙并打开信箱，这就是锁的作用了。更何况咱们俩是不能直接见面的，所以这个信箱本身就可以被视为一个临界区。</p><p>尽管没有协调好，咱们俩仍然要想方设法的完成任务啊。所以，如果信箱里有情报，而你却迟迟未取走，那我就需要每过一段时间带着新情报去检查一次，若发现信箱空了，我就需要及时地把新情报放到里面。</p><p>另一方面，如果信箱里一直没有情报，那你也要每过一段时间去打开看看，一旦有了情报就及时地取走。这么做是可以的，但就是太危险了，很容易被敌人发现。</p><p>后来，我们又想了一个计策，各自雇佣了一个不起眼的小孩儿。如果早上七点有一个戴红色帽子的小孩儿从你家楼下路过，那么就意味着信箱里有了新情报。另一边，如果上午九点有一个戴蓝色帽子的小孩儿从我家楼下路过，那就说明你已经从信箱中取走了情报。</p><p>这样一来，咱们执行任务的隐蔽性高多了，并且效率的提升非常显著。这两个戴不同颜色帽子的小孩儿就相当于条件变量，在共享资源的状态产生变化的时候，起到了通知的作用。</p><p>当然了，我们是在用 Go 语言编写程序，而不是在执行什么秘密任务。因此，条件变量在这里的最大优势就是在效率方面的提升。当共享资源的状态不满足条件的时候，想操作它的线程再也不用循环往复地做检查了，只要等待通知就好了。</p><h3 id="今天的问题是：条件变量怎样与互斥锁配合使用？"><a href="#今天的问题是：条件变量怎样与互斥锁配合使用？" class="headerlink" title="今天的问题是：条件变量怎样与互斥锁配合使用？"></a>今天的问题是：条件变量怎样与互斥锁配合使用？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>条件变量的初始化离不开互斥锁，并且它的方法有的也是基于互斥锁的。</p><p>条件变量提供的方法有三个：等待通知（wait）、单发通知（signal）和广播通知（broadcast）。</p><p>我们在利用条件变量等待通知的时候，需要在它基于的那个互斥锁保护下进行。而在进行单发通知或广播通知的时候，却是恰恰相反的，也就是说，需要在对应的互斥锁解锁之后再做这两种操作。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>这个问题看起来很简单，但其实可以基于它, 延伸出很多其他的问题。比如，每个方法的使用时机是什么？又比如，每个方法执行的内部流程是怎样的？</p><p>下面，我们一边用代码实现前面那个例子，一边讨论条件变量的使用。</p><p>首先，我们先来创建如下几个变量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> mailbox <span class="type">uint8</span></span><br><span class="line"><span class="keyword">var</span> lock sync.RWMutex</span><br><span class="line">sendCond := sync.NewCond(&amp;lock)</span><br><span class="line">recvCond := sync.NewCond(lock.RLocker())</span><br></pre></td></tr></table></figure><p>变量mailbox代表信箱，是uint8类型的。 若它的值为0则表示信箱中没有情报，而当它的值为1时则说明信箱中有情报。lock是一个类型为sync.RWMutex的变量，是一个读写锁，也可以被视为信箱上的那把锁。</p><p>另外，基于这把锁，我还创建了两个代表条件变量的变量，名字分别叫sendCond和recvCond。 它们都是*sync.Cond类型的，同时也都是由sync.NewCond函数来初始化的。</p><p>与sync.Mutex类型和sync.RWMutex类型不同，sync.Cond类型并不是开箱即用的。我们只能利用sync.NewCond函数创建它的指针值。这个函数需要一个sync.Locker类型的参数值。</p><p>还记得吗？我在前面说过，条件变量是基于互斥锁的，它必须有互斥锁的支撑才能够起作用。因此，这里的参数值是不可或缺的，它会参与到条件变量的方法实现当中。</p><p>sync.Locker其实是一个接口，在它的声明中只包含了两个方法定义，即：Lock()和Unlock()。sync.Mutex类型和sync.RWMutex类型都拥有Lock方法和Unlock方法，只不过它们都是指针方法。因此，这两个类型的指针类型才是sync.Locker接口的实现类型。</p><p>我在为sendCond变量做初始化的时候，把基于lock变量的指针值传给了sync.NewCond函数。</p><p>原因是，lock变量的Lock方法和Unlock方法分别用于对其中写锁的锁定和解锁，它们与sendCond变量的含义是对应的。sendCond是专门为放置情报而准备的条件变量，向信箱里放置情报，可以被视为对共享资源的写操作。</p><p>相应的，recvCond变量代表的是专门为获取情报而准备的条件变量。 虽然获取情报也会涉及对信箱状态的改变，但是好在做这件事的人只会有你一个，而且我们也需要借此了解一下，条件变量与读写锁中的读锁的联用方式。所以，在这里，我们暂且把获取情报看做是对共享资源的读操作。</p><p>因此，为了初始化recvCond这个条件变量，我们需要的是lock变量中的读锁，并且还需要是sync.Locker类型的。</p><p>可是，lock变量中用于对读锁进行锁定和解锁的方法却是RLock和RUnlock，它们与sync.Locker接口中定义的方法并不匹配。</p><p>好在sync.RWMutex类型的RLocker方法可以实现这一需求。我们只要在调用sync.NewCond函数时，传入调用表达式lock.RLocker()的结果值，就可以使该函数返回符合要求的条件变量了。</p><p>为什么说通过lock.RLocker()得来的值就是lock变量中的读锁呢？实际上，这个值所拥有的Lock方法和Unlock方法，在其内部会分别调用lock变量的RLock方法和RUnlock方法。也就是说，前两个方法仅仅是后两个方法的代理而已。</p><p>好了，我们现在有四个变量。一个是代表信箱的mailbox，一个是代表信箱上的锁的lock。还有两个是，代表了蓝帽子小孩儿的sendCond，以及代表了红帽子小孩儿的recvCond。</p><p><img src="https://static001.geekbang.org/resource/image/36/5d/3619456ade9d45a4d9c0fbd22bb6fd5d.png" alt="互斥锁与条件变量"></p><p>我，现在是一个 goroutine（携带的go函数），想要适时地向信箱里放置情报并通知你，应该怎么做呢？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">lock.Lock()</span><br><span class="line"><span class="keyword">for</span> mailbox == <span class="number">1</span> &#123;</span><br><span class="line"> sendCond.Wait()</span><br><span class="line">&#125;</span><br><span class="line">mailbox = <span class="number">1</span></span><br><span class="line">lock.Unlock()</span><br><span class="line">recvCond.Signal()</span><br></pre></td></tr></table></figure><p>我肯定需要先调用lock变量的Lock方法。注意，这个Lock方法在这里意味的是：持有信箱上的锁，并且有打开信箱的权利，而不是锁上这个锁。</p><p>然后，我要检查mailbox变量的值是否等于1，也就是说，要看看信箱里是不是还存有情报。如果还有情报，那么我就回家去等蓝帽子小孩儿了。</p><p>这就是那条for语句以及其中的调用表达式sendCond.Wait()所表示的含义了。你可能会问，为什么这里是for语句而不是if语句呢？我在后面会对此进行解释的。</p><p>我们再往后看，如果信箱里没有情报，那么我就把新情报放进去，关上信箱、锁上锁，然后离开。用代码表达出来就是mailbox &#x3D; 1和lock.Unlock()。</p><p>离开之后我还要做一件事，那就是让红帽子小孩儿准时去你家楼下路过。也就是说，我会及时地通知你“信箱里已经有新情报了”，我们调用recvCond的Signal方法就可以实现这一步骤。</p><p>另一方面，你现在是另一个 goroutine，想要适时地从信箱中获取情报，然后通知我。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">lock.RLock()</span><br><span class="line"><span class="keyword">for</span> mailbox == <span class="number">0</span> &#123;</span><br><span class="line"> recvCond.Wait()</span><br><span class="line">&#125;</span><br><span class="line">mailbox = <span class="number">0</span></span><br><span class="line">lock.RUnlock()</span><br><span class="line">sendCond.Signal()</span><br></pre></td></tr></table></figure><p>你跟我做的事情在流程上其实基本一致，只不过每一步操作的对象是不同的。你需要调用的是lock变量的RLock方法。因为你要进行的是读操作，并且会使用recvCond变量作为辅助。recvCond与lock变量的读锁是对应的。</p><p>在打开信箱后，你要关注的是信箱里是不是没有情报，也就是检查mailbox变量的值是否等于0。如果它确实等于0，那么你就需要回家去等红帽子小孩儿，也就是调用recvCond的Wait方法。这里使用的依然是for语句。</p><p>如果信箱里有情报，那么你就应该取走情报，关上信箱、锁上锁，然后离开。对应的代码是mailbox &#x3D; 0和lock.RUnlock()。之后，你还需要让蓝帽子小孩儿准时去我家楼下路过。这样我就知道信箱中的情报已经被你获取了。</p><p>以上这些，就是对咱们俩要执行秘密任务的代码实现。其中的条件变量的用法需要你特别注意。</p><p>再强调一下，只要条件不满足，我就会通过调用sendCond变量的Wait方法，去等待你的通知，只有在收到通知之后我才会再次检查信箱。</p><p>另外，当我需要通知你的时候，我会调用recvCond变量的Signal方法。你使用这两个条件变量的方式正好与我相反。你可能也看出来了，利用条件变量可以实现单向的通知，而双向的通知则需要两个条件变量。这也是条件变量的基本使用规则。</p><p>完整代码如下</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line">    <span class="string">&quot;sync&quot;</span></span><br><span class="line">    <span class="string">&quot;time&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// mailbox 代表信箱。</span></span><br><span class="line">    <span class="comment">// 0代表信箱是空的，1代表信箱是满的。</span></span><br><span class="line">    <span class="keyword">var</span> mailbox <span class="type">uint8</span></span><br><span class="line">    <span class="comment">// lock 代表信箱上的锁。</span></span><br><span class="line">    <span class="keyword">var</span> lock sync.RWMutex</span><br><span class="line">    <span class="comment">// sendCond 代表专用于发信的条件变量。</span></span><br><span class="line">    sendCond := sync.NewCond(&amp;lock)</span><br><span class="line">    <span class="comment">// recvCond 代表专用于收信的条件变量。</span></span><br><span class="line">    recvCond := sync.NewCond(lock.RLocker())</span><br><span class="line"></span><br><span class="line">    <span class="comment">// sign 用于传递演示完成的信号。</span></span><br><span class="line">    sign := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;, <span class="number">3</span>)</span><br><span class="line">    max := <span class="number">5</span></span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(max <span class="type">int</span>)</span></span> &#123; <span class="comment">// 用于发信。</span></span><br><span class="line">        <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">            sign &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">        &#125;()</span><br><span class="line">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= max; i++ &#123;</span><br><span class="line">            time.Sleep(time.Millisecond * <span class="number">500</span>)</span><br><span class="line">            lock.Lock()</span><br><span class="line">            <span class="keyword">for</span> mailbox == <span class="number">1</span> &#123;</span><br><span class="line">                sendCond.Wait()</span><br><span class="line">            &#125;</span><br><span class="line">            log.Printf(<span class="string">&quot;sender [%d]: the mailbox is empty.&quot;</span>, i)</span><br><span class="line">            mailbox = <span class="number">1</span></span><br><span class="line">            log.Printf(<span class="string">&quot;sender [%d]: the letter has been sent.&quot;</span>, i)</span><br><span class="line">            lock.Unlock()</span><br><span class="line">            recvCond.Signal()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;(max)</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(max <span class="type">int</span>)</span></span> &#123; <span class="comment">// 用于收信。</span></span><br><span class="line">        <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">            sign &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">        &#125;()</span><br><span class="line">        <span class="keyword">for</span> j := <span class="number">1</span>; j &lt;= max; j++ &#123;</span><br><span class="line">            time.Sleep(time.Millisecond * <span class="number">500</span>)</span><br><span class="line">            lock.RLock()</span><br><span class="line">            <span class="keyword">for</span> mailbox == <span class="number">0</span> &#123;</span><br><span class="line">                recvCond.Wait()</span><br><span class="line">            &#125;</span><br><span class="line">            log.Printf(<span class="string">&quot;receiver [%d]: the mailbox is full.&quot;</span>, j)</span><br><span class="line">            mailbox = <span class="number">0</span></span><br><span class="line">            log.Printf(<span class="string">&quot;receiver [%d]: the letter has been received.&quot;</span>, j)</span><br><span class="line">            lock.RUnlock()</span><br><span class="line">            sendCond.Signal()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;(max)</span><br><span class="line"></span><br><span class="line">    &lt;-sign</span><br><span class="line">    &lt;-sign</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-tiao-jian-bian-liang-sync.cond-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-tiao-jian-bian-liang-sync.cond-shang/"/>
    <published>2021-11-24T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="前导知识：条件变量与互斥锁"><a href="#前导知识：条件变量与互斥锁" class="headerlink" title="前导知识：条件变量与互斥锁"></a>前导知识：条件变量与互斥锁</h3><p>我们常常会把条件变量这个同步工具拿来与互斥锁一起讨论。实际上，条件变量是基于互斥锁的，它必须有互斥锁的支撑才能发挥作用。</p>
<p>条件变量并不是被用来保护临界区和共享资源的，它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时，它可以被用来通知被互斥锁阻塞的线程。</p>]]>
    </summary>
    <title>Go语言核心36讲-条件变量sync.Cond（上）</title>
    <updated>2026-04-13T10:24:15.853Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：条件变量的Wait方法做了什么？"><a href="#问题-1：条件变量的Wait方法做了什么？" class="headerlink" title="问题 1：条件变量的Wait方法做了什么？"></a>问题 1：条件变量的Wait方法做了什么？</h4><p>在了解了条件变量的使用方式之后，你可能会有这么几个疑问。</p><ol><li>为什么先要锁定条件变量基于的互斥锁，才能调用它的Wait方法？</li><li>为什么要用for语句来包裹调用其Wait方法的表达式，用if语句不行吗？</li></ol><span id="more"></span><p>这些问题我在面试的时候也经常问。你需要对这个Wait方法的内部机制有所了解才能回答上来。</p><p>条件变量的Wait方法主要做了四件事。</p><ol><li>把调用它的 goroutine（也就是当前的 goroutine）加入到当前条件变量的通知队列中。</li><li>解锁当前的条件变量基于的那个互斥锁。</li><li>让当前的 goroutine 处于等待状态，等到通知到来时再决定是否唤醒它。此时，这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。</li><li>如果通知到来并且决定唤醒这个 goroutine，那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后，当前的 goroutine 就会继续执行后面的代码了。</li></ol><p>因为条件变量的Wait方法在阻塞当前的 goroutine 之前，会解锁它基于的互斥锁，所以在调用该Wait方法之前，我们必须先锁定那个互斥锁，否则在调用这个Wait方法时，就会引发一个不可恢复的 panic。</p><p>为什么条件变量的Wait方法要这么做呢？你可以想象一下，如果Wait方法在互斥锁已经锁定的情况下，阻塞了当前的 goroutine，那么又由谁来解锁呢？别的 goroutine 吗？</p><p>先不说这违背了互斥锁的重要使用原则，即：成对的锁定和解锁，就算别的 goroutine 可以来解锁，那万一解锁重复了怎么办？由此引发的 panic 可是无法恢复的。</p><p>如果当前的 goroutine 无法解锁，别的 goroutine 也都不来解锁，那么又由谁来进入临界区，并改变共享资源的状态呢？只要共享资源的状态不变，即使当前的 goroutine 因收到通知而被唤醒，也依然会再次执行这个Wait方法，并再次被阻塞。</p><p>所以说，如果条件变量的Wait方法不先解锁互斥锁的话，那么就只会造成两种后果：不是当前的程序因 panic 而崩溃，就是相关的 goroutine 全面阻塞。</p><p>再解释第二个疑问。很显然，if语句只会对共享资源的状态检查一次，而for语句却可以做多次检查，直到这个状态改变为止。那为什么要做多次检查呢？</p><p><strong>这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒，但却发现共享资源的状态，依然不符合它的要求，那么就应该再次调用条件变量的Wait方法，并继续等待下次通知的到来。</strong></p><p>这种情况是很有可能发生的，具体如下面所示。</p><ol><li>有多个 goroutine 在等待共享资源的同一种状态。比如，它们都在等mailbox变量的值不为0的时候再把它的值变为0，这就相当于有多个人在等着我向信箱里放置情报。虽然等待的 goroutine 有多个，但每次成功的 goroutine 却只可能有一个。别忘了，条件变量的Wait方法会在当前的 goroutine 醒来后先重新锁定那个互斥锁。在成功的 goroutine 最终解锁互斥锁之后，其他的 goroutine 会先后进入临界区，但它们会发现共享资源的状态依然不是它们想要的。这个时候，for循环就很有必要了。</li><li>共享资源可能有的状态不是两个，而是更多。比如，mailbox变量的可能值不只有0和1，还有2、3、4。这种情况下，由于状态在每次改变后的结果只可能有一个，所以，在设计合理的前提下，单一的结果一定不可能满足所有 goroutine 的条件。那些未被满足的 goroutine 显然还需要继续等待和检查。</li><li>有一种可能，共享资源的状态只有两个，并且每种状态都只有一个 goroutine 在关注，就像我们在主问题当中实现的那个例子那样。不过，即使是这样，使用for语句仍然是有必要的。原因是，在一些多 CPU 核心的计算机系统中，即使没有收到条件变量的通知，调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的，即使是操作系统（比如 Linux）本身提供的条件变量也会如此。</li></ol><p>综上所述，在包裹条件变量的Wait方法的时候，我们总是应该使用for语句。</p><h4 id="问题-2：条件变量的Signal方法和Broadcast方法有哪些异同？"><a href="#问题-2：条件变量的Signal方法和Broadcast方法有哪些异同？" class="headerlink" title="问题 2：条件变量的Signal方法和Broadcast方法有哪些异同？"></a>问题 2：条件变量的Signal方法和Broadcast方法有哪些异同？</h4><p>条件变量的Signal方法和Broadcast方法都是被用来发送通知的，不同的是，前者的通知只会唤醒一个因此而等待的 goroutine，而后者的通知却会唤醒所有为此等待的 goroutine。</p><p>条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾，而它的Signal方法总会从通知队列的队首开始，查找可被唤醒的 goroutine。所以，因Signal方法的通知，而被唤醒的 goroutine 一般都是最早等待的那一个。</p><p>这两个方法的行为决定了它们的适用场景。如果你确定只有一个 goroutine 在等待通知，或者只需唤醒任意一个 goroutine 就可以满足要求，那么使用条件变量的Signal方法就好了。</p><p>否则，使用Broadcast方法总没错，只要你设置好各个 goroutine 所期望的共享资源状态就可以了。此外，再次强调一下，与Wait方法不同，条件变量的Signal方法和Broadcast方法并不需要在互斥锁的保护下执行。恰恰相反，我们最好在解锁条件变量基于的那个互斥锁之后，再去调用它的这两个方法。这更有利于程序的运行效率。</p><p>最后，请注意，条件变量的通知具有即时性。也就是说，如果发送通知的时候没有 goroutine 为此等待，那么该通知就会被直接丢弃。在这之后才开始等待的 goroutine 只可能被后面的通知唤醒。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-tiao-jian-bian-liang-sync.cond-xia/</id>
    <link href="http://blog.chcaty.cn/2021/11/24/go-yu-yan-he-xin-36-jiang-tiao-jian-bian-liang-sync.cond-xia/"/>
    <published>2021-11-24T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：条件变量的Wait方法做了什么？"><a href="#问题-1：条件变量的Wait方法做了什么？" class="headerlink" title="问题 1：条件变量的Wait方法做了什么？"></a>问题 1：条件变量的Wait方法做了什么？</h4><p>在了解了条件变量的使用方式之后，你可能会有这么几个疑问。</p>
<ol>
<li>为什么先要锁定条件变量基于的互斥锁，才能调用它的Wait方法？</li>
<li>为什么要用for语句来包裹调用其Wait方法的表达式，用if语句不行吗？</li>
</ol>]]>
    </summary>
    <title>Go语言核心36讲-条件变量sync.Cond（下）</title>
    <updated>2026-04-13T10:24:15.854Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>从本篇文章开始，我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包。这会涉及这些代码包的标准用法、使用禁忌、背后原理以及周边的知识。</p><span id="more"></span><h3 id="前导内容：竞态条件、临界区与同步工具"><a href="#前导内容：竞态条件、临界区与同步工具" class="headerlink" title="前导内容：竞态条件、临界区与同步工具"></a>前导内容：竞态条件、临界区与同步工具</h3><p>我们首先要看的就是sync包。这里的“sync”的中文意思是“同步”。我们下面就从同步讲起。</p><p>相比于 Go 语言宣扬的“用通讯的方式共享数据”，通过共享数据的方式来传递信息和协调线程运行的做法其实更加主流，毕竟大多数的现代编程语言，都是用后一种方式作为并发编程的解决方案的（这种方案的历史非常悠久，恐怕可以追溯到上个世纪多进程编程时代伊始了）。</p><p>一旦数据被多个线程共享，那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件（race condition），这往往会破坏共享数据的一致性。</p><p>共享数据的一致性代表着某种约定，即：多个线程对共享数据的操作总是可以达到它们各自预期的效果。</p><p>如果这个一致性得不到保证，那么将会影响到一些线程中代码和流程的正确执行，甚至会造成某种不可预知的错误。这种错误一般都很难发现和定位，排查起来的成本也是非常高的，所以一定要尽量避免。</p><p>举个例子，同时有多个线程连续向同一个缓冲区写入数据块，如果没有一个机制去协调这些线程的写入操作的话，那么被写入的数据块就很可能会出现错乱。比如，在线程 A 还没有写完一个数据块的时候，线程 B 就开始写入另外一个数据块了。</p><p>显然，这两个数据块中的数据会被混在一起，并且已经很难分清了。因此，在这种情况下，我们就需要采取一些措施来协调它们对缓冲区的修改。这通常就会涉及同步。</p><p>概括来讲，<strong>同步的用途有两个，一个是避免多个线程在同一时刻操作同一个数据块，另一个是协调多个线程，以避免它们在同一时刻执行同一个代码块。</strong></p><p>由于这样的数据块和代码块的背后都隐含着一种或多种资源（比如存储资源、计算资源、I&#x2F;O 资源、网络资源等等），所以我们可以把它们看做是共享资源，或者说共享资源的代表。我们所说的同步其实就是在控制多个线程对共享资源的访问。</p><p>一个线程在想要访问某一个共享资源的时候，需要先申请对该资源的访问权限，并且只有在申请成功之后，访问才能真正开始。</p><p>而当线程对共享资源的访问结束时，它还必须归还对该资源的访问权限，若要再次访问仍需申请。你可以把这里所说的访问权限想象成一块令牌，线程一旦拿到了令牌，就可以进入指定的区域，从而访问到资源，而一旦线程要离开这个区域了，就需要把令牌还回去，绝不能把令牌带走。</p><p>如果针对某个共享资源的访问令牌只有一块，那么在同一时刻，就最多只能有一个线程进入到那个区域，并访问到该资源。</p><p>这时，我们可以说，多个并发运行的线程对这个共享资源的访问是完全串行的。只要一个代码片段需要实现对共享资源的串行化访问，就可以被视为一个临界区（critical section），也就是我刚刚说的，由于要访问到资源而必须进入的那个区域。</p><p>比如，在我前面举的那个例子中，实现了数据块写入操作的代码就共同组成了一个临界区。如果针对同一个共享资源，这样的代码片段有多个，那么它们就可以被称为相关临界区。</p><p>它们可以是一个内含了共享数据的结构体及其方法，也可以是操作同一块共享数据的多个函数。临界区总是需要受到保护的，否则就会产生竞态条件。<strong>施加保护的重要手段之一，就是使用实现了某种同步机制的工具，也称为同步工具。</strong><br><img src="https://static001.geekbang.org/resource/image/73/6c/73d3313640e62bb95855d40c988c2e6c.png" alt="竞态条件、临界区与同步工具"></p><p>在 Go 语言中，可供我们选择的同步工具并不少。其中，最重要且最常用的同步工具当属互斥量（mutual exclusion，简称 mutex）。sync包中的Mutex就是与其对应的类型，该类型的值可以被称为互斥量或者互斥锁。</p><p>一个互斥锁可以被用来保护一个临界区或者一组相关临界区。我们可以通过它来保证，在同一时刻只有一个 goroutine 处于该临界区之内。为了兑现这个保证，每当有 goroutine 想进入临界区时，都需要先对它进行锁定，并且，每个 goroutine 离开临界区时，都要及时地对它进行解锁。</p><p>锁定操作可以通过调用互斥锁的Lock方法实现，而解锁操作可以调用互斥锁的Unlock方法。重点代码片段：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">mu.Lock()</span><br><span class="line">_, err := writer.Write([]<span class="type">byte</span>(data))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"> log.Printf(<span class="string">&quot;error: %s [%d]&quot;</span>, err, id)</span><br><span class="line">&#125;</span><br><span class="line">mu.Unlock()</span><br></pre></td></tr></table></figure><h3 id="今天的问题是：我们使用互斥锁时有哪些注意事项？"><a href="#今天的问题是：我们使用互斥锁时有哪些注意事项？" class="headerlink" title="今天的问题是：我们使用互斥锁时有哪些注意事项？"></a>今天的问题是：我们使用互斥锁时有哪些注意事项？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>使用互斥锁的注意事项如下：</p><ol><li>不要重复锁定互斥锁；</li><li>不要忘记解锁互斥锁，必要时使用defer语句；</li><li>不要对尚未锁定或者已解锁的互斥锁解锁；</li><li>不要在多个函数之间直接传递互斥锁。</li></ol><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>首先，你还是要把互斥锁看作是针对某一个临界区或某一组相关临界区的唯一访问令牌。</p><p>虽然没有任何强制规定来限制，你用同一个互斥锁保护多个无关的临界区，但是这样做，一定会让你的程序变得很复杂，并且也会明显地增加你的心智负担。</p><p>你要知道，对一个已经被锁定的互斥锁进行锁定，是会立即阻塞当前的 goroutine 的。这个 goroutine 所执行的流程，会一直停滞在调用该互斥锁的Lock方法的那行代码上。</p><p>直到该互斥锁的Unlock方法被调用，并且这里的锁定操作成功完成，后续的代码（也就是临界区中的代码）才会开始执行。这也正是互斥锁能够保护临界区的原因所在。</p><p>一旦，你把一个互斥锁同时用在了多个地方，就必然会有更多的 goroutine 争用这把锁。这不但会让你的程序变慢，还会大大增加死锁（deadlock）的可能性。</p><p>所谓的死锁，指的就是当前程序中的主 goroutine，以及我们启用的那些 goroutine 都已经被阻塞。这些 goroutine 可以被统称为用户级的 goroutine。这就相当于整个程序都已经停滞不前了。</p><p>Go 语言运行时系统是不允许这种情况出现的，只要它发现所有的用户级 goroutine 都处于等待状态，就会自行抛出一个带有如下信息的 panic：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fatal <span class="type">error</span>: all goroutines are asleep - deadlock!</span><br></pre></td></tr></table></figure><p><strong>注意，这种由 Go 语言运行时系统自行抛出的 panic 都属于致命错误，都是无法被恢复的，调用recover函数对它们起不到任何作用。也就是说，一旦产生死锁，程序必然崩溃。</strong></p><p>因此，我们一定要尽量避免这种情况的发生。而最简单、有效的方式就是让每一个互斥锁都只保护一个临界区或一组相关临界区。</p><p>在这个前提之下，我们还需要注意，对于同一个 goroutine 而言，既不要重复锁定一个互斥锁，也不要忘记对它进行解锁。</p><p>一个 goroutine 对某一个互斥锁的重复锁定，就意味着它自己锁死了自己。先不说这种做法本身就是错误的，在这种情况下，想让其他的 goroutine 来帮它解锁是非常难以保证其正确性的。</p><p>说“不要忘记解锁互斥锁”的一个很重要的原因就是：<strong>避免重复锁定</strong>。</p><p>因为在一个 goroutine 执行的流程中，可能会出现诸如“锁定、解锁、再锁定、再解锁”的操作，所以如果我们忘记了中间的解锁操作，那就一定会造成重复锁定。</p><p>除此之外，忘记解锁还会使其他的 goroutine 无法进入到该互斥锁保护的临界区，这轻则会导致一些程序功能的失效，重则会造成死锁和程序崩溃。</p><p>在很多时候，一个函数执行的流程并不是单一的，流程中间可能会有分叉，也可能会被中断。</p><p>如果一个流程在锁定了某个互斥锁之后分叉了，或者有被中断的可能，那么就应该使用defer语句来对它进行解锁，而且这样的defer语句应该紧跟在锁定操作之后。这是最保险的一种做法。</p><p>忘记解锁导致的问题有时候是比较隐秘的，并不会那么快就暴露出来。这也是我们需要特别关注它的原因。相比之下，解锁未锁定的互斥锁会立即引发 panic。</p><p>并且，与死锁导致的 panic 一样，它们是无法被恢复的。<strong>因此，我们总是应该保证，对于每一个锁定操作，都要有且只有一个对应的解锁操作。</strong></p><p>换句话说，我们应该让它们成对出现。这也算是互斥锁的一个很重要的使用原则了。在很多时候，利用defer语句进行解锁可以更容易做到这一点。</p><p><img src="https://static001.geekbang.org/resource/image/4f/0d/4f86467d09ffca6e0c02602a9cb7480d.png" alt="互斥锁的重复锁定和重复解锁"></p><p>Go 语言中的互斥锁是开箱即用的。换句话说，一旦我们声明了一个sync.Mutex类型的变量，就可以直接使用它了。</p><p>不过要注意，该类型是一个结构体类型，属于值类型中的一种。把它传给一个函数、将它从函数中返回、把它赋给其他变量、让它进入某个通道都会导致它的副本的产生。</p><p>并且，原值和它的副本，以及多个副本之间都是完全独立的，它们都是不同的互斥锁。</p><p>如果你把一个互斥锁作为参数值传给了一个函数，那么在这个函数中对传入的锁的所有操作，都不会对存在于该函数之外的那个原锁产生任何的影响。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：读写锁与互斥锁有哪些异同？"><a href="#问题-1：读写锁与互斥锁有哪些异同？" class="headerlink" title="问题 1：读写锁与互斥锁有哪些异同？"></a>问题 1：读写锁与互斥锁有哪些异同？</h4><p>读写锁是读 &#x2F; 写互斥锁的简称。在 Go 语言中，读写锁由sync.RWMutex类型的值代表。与sync.Mutex类型一样，这个类型也是开箱即用的。</p><p>顾名思义，读写锁是把对共享资源的“读操作”和“写操作”区别对待了。它可以对这两种操作施加不同程度的保护。换句话说，相比于互斥锁，读写锁可以实现更加细腻的访问控制。</p><p>一个读写锁中实际上包含了两个锁，即：读锁和写锁。sync.RWMutex类型中的Lock方法和Unlock方法分别用于对写锁进行锁定和解锁，而它的RLock方法和RUnlock方法则分别用于对读锁进行锁定和解锁。</p><p>另外，对于同一个读写锁来说有如下规则。</p><ol><li>在写锁已被锁定的情况下再试图锁定写锁，会阻塞当前的 goroutine。</li><li>在写锁已被锁定的情况下试图锁定读锁，也会阻塞当前的 goroutine。</li><li>在读锁已被锁定的情况下试图锁定写锁，同样会阻塞当前的 goroutine。</li><li>在读锁已被锁定的情况下再试图锁定读锁，并不会阻塞当前的 goroutine。</li></ol><p>换一个角度来说，对于某个受到读写锁保护的共享资源，多个写操作不能同时进行，写操作和读操作也不能同时进行，但多个读操作却可以同时进行。</p><p>当然了，只有在我们正确使用读写锁的情况下，才能达到这种效果。还是那句话，我们需要让每一个锁都只保护一个临界区，或者一组相关临界区，并以此尽量减少误用的可能性。顺便说一句，我们通常把这种不能同时进行的操作称为互斥操作。</p><p>再来看另一个方面。对写锁进行解锁，会唤醒“所有因试图锁定读锁，而被阻塞的 goroutine”，并且，这通常会使它们都成功完成对读锁的锁定。</p><p>然而，对读锁进行解锁，只会在没有其他读锁锁定的前提下，唤醒“因试图锁定写锁，而被阻塞的 goroutine”；并且，最终只会有一个被唤醒的 goroutine 能够成功完成对写锁的锁定，其他的 goroutine 还要在原处继续等待。至于是哪一个 goroutine，那就要看谁的等待时间最长了。</p><p>除此之外，读写锁对写操作之间的互斥，其实是通过它内含的一个互斥锁实现的。因此，也可以说，Go 语言的读写锁是互斥锁的一种扩展。</p><p>最后，需要强调的是，与互斥锁类似，解锁“读写锁中未被锁定的写锁”，会立即引发 panic，对于其中的读锁也是如此，并且同样是不可恢复的。</p><p>总之，读写锁与互斥锁的不同，都源于它把对共享资源的写操作和读操作区别对待了。这也使得它实现的互斥规则要更复杂一些。</p><p>不过，正因为如此，我们可以使用它对共享资源的操作，实行更加细腻的控制。另外，由于这里的读写锁是互斥锁的一种扩展，所以在有些方面它还是沿用了互斥锁的行为模式。比如，在解锁未锁定的写锁或读锁时的表现，又比如，对写操作之间互斥的实现方式。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-sync.mutex-yu-sync.rwmutex/</id>
    <link href="http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-sync.mutex-yu-sync.rwmutex/"/>
    <published>2021-11-23T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>从本篇文章开始，我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包。这会涉及这些代码包的标准用法、使用禁忌、背后原理以及周边的知识。</p>]]>
    </summary>
    <title>Go语言核心36讲-sync.Mutex与sync.RWMutex</title>
    <updated>2026-04-13T10:24:15.850Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>在本篇文章，我会继续为你讲解更多更高级的测试方法。这会涉及testing包中更多的 API、go test命令支持的，更多标记更加复杂的测试结果，以及测试覆盖度分析等等。</p><span id="more"></span><h3 id="前导内容：-cpu-的功能"><a href="#前导内容：-cpu-的功能" class="headerlink" title="前导内容：-cpu 的功能"></a>前导内容：-cpu 的功能</h3><p>go test命令的标记-cpu，它是用来设置测试执行最大 P 数量的列表的。</p><blockquote><p>这里的 P 是 processor 的缩写，每个 processor 都是一个可以承载若干个 G，且能够使这些 G 适时地与 M 进行对接并得到真正运行的中介。<br>正是由于 P 的存在，G 和 M 才可以呈现出多对多的关系，并能够及时、灵活地进行组合和分离。<br>这里的 G 就是 goroutine 的缩写，可以被理解为 Go 语言自己实现的用户级线程。M 即为 machine 的缩写，代表着系统级线程，或者说操作系统内核级别的线程。</p></blockquote><p>Go 语言并发编程模型中的 P，正是 goroutine 的数量能够数十万计的关键所在。P 的数量意味着 Go 程序背后的运行时系统中，会有多少个用于承载可运行的 G 的队列存在。</p><p>每一个队列都相当于一条流水线，它会源源不断地把可运行的 G 输送给空闲的 M，并使这两者对接。</p><p>一旦对接完成，被对接的 G 就真正地运行在操作系统的内核级线程之上了。每条流水线之间虽然会有联系，但都是独立运作的。</p><p>因此，最大 P 数量就代表着 Go 语言运行时系统同时运行 goroutine 的能力，也可以被视为其中逻辑 CPU 的最大个数。而go test命令的-cpu标记正是用于设置这个最大个数的。</p><p>也许你已经知道，在默认情况下，最大 P 数量就等于当前计算机 CPU 核心的实际数量。</p><p>当然了，前者也可以大于或者小于后者，如此可以在一定程度上模拟拥有不同的 CPU 核心数的计算机。</p><p>所以，也可以说，使用-cpu标记可以模拟：被测程序在计算能力不同计算机中的表现。</p><h3 id="今天的问题是：怎样设置-cpu标记的值，以及它会对测试流程产生什么样的影响？"><a href="#今天的问题是：怎样设置-cpu标记的值，以及它会对测试流程产生什么样的影响？" class="headerlink" title="今天的问题是：怎样设置-cpu标记的值，以及它会对测试流程产生什么样的影响？"></a>今天的问题是：怎样设置-cpu标记的值，以及它会对测试流程产生什么样的影响？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>标记-cpu的值应该是一个正整数的列表，该列表的表现形式为：以英文半角逗号分隔的多个整数字面量，比如1,2,4。</p><p>针对于此值中的每一个正整数，go test命令都会先设置最大 P 数量为该数，然后再执行测试函数。</p><p>如果测试函数有多个，那么go test命令会依照此方式逐个执行。</p><blockquote><p>以1,2,4为例，go test命令会先以1,2,4为最大 P 数量分别去执行第一个测试函数，之后再用同样的方式执行第二个测试函数，以此类推。</p></blockquote><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>实际上，不论我们是否追加了-cpu标记，go test命令执行测试函数时流程都是相同的，只不过具体执行步骤会略有不同。</p><p>go test命令在进行准备工作的时候会读取-cpu标记的值，并把它转换为一个以int为元素类型的切片，我们也可以称它为逻辑 CPU 切片。</p><p>如果该命令发现我们并没有追加这个标记，那么就会让逻辑 CPU 切片只包含一个元素值，即最大 P 数量的默认值，也就是当前计算机 CPU 核心的实际数量。</p><p>在准备执行某个测试函数的时候，无论该函数是功能测试函数，还是性能测试函数，go test命令都会迭代逻辑 CPU 切片，并且在每次迭代时，先依据当前的元素值设置最大 P 数量，然后再去执行测试函数。</p><p>注意，对于性能测试函数来说，这里可能不只执行了一次。你还记得测试函数的执行时间上限，以及那个由b.N代表的被测程序的执行次数吗？</p><p>概括来讲，go test命令每一次对性能测试函数的执行，都是一个探索的过程。它会在测试函数的执行时间上限不变的前提下，尝试找到被测程序的最大执行次数。</p><p>在这个过程中，性能测试函数可能会被执行多次。为了以后描述方便，我们把这样一个探索的过程称为：对性能测试函数的一次探索式执行，这其中包含了对该函数的若干次执行，当然，肯定也包括了对被测程序更多次的执行。</p><p>说到多次执行测试函数，我们就不得不提及另外一个标记，即-count。-count标记是专门用于重复执行测试函数的。它的值必须大于或等于0，并且默认值为1。</p><p>如果我们在运行go test命令的时候追加了-count 5，那么对于每一个测试函数，命令都会在预设的不同条件下（比如不同的最大 P 数量下）分别重复执行五次。</p><p>如果我们把前文所述的-cpu标记、-count标记，以及探索式执行联合起来看，就可以用一个公式来描述单个性能测试函数，在go test命令的一次运行过程中的执行次数，即：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">性能测试函数的执行次数 = `-cpu`标记的值中正整数的个数 x `-count`标记的值 x 探索式执行中测试函数的实际执行次数</span><br></pre></td></tr></table></figure><p>对于功能测试函数来说，这个公式会更加简单一些，即:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">功能测试函数的执行次数 = `-cpu`标记的值中正整数的个数 x `-count`标记的值</span><br></pre></td></tr></table></figure><p><img src="https://static001.geekbang.org/resource/image/8d/56/8dc543c7ac67dca3dae3eebc53067c56.png?wh=1881*1081" alt="测试函数的实际执行次数"></p><p>看完了这两个公式，我想，你也许遇到过这种情况，在对 Go 程序执行某种自动化测试的过程中，测试日志会显得特别多，而且好多都是重复的。</p><p>这时，我们首先就应该想到，上面这些导致测试函数多次执行的标记和流程。我们往往需要检查这些标记的使用是否合理、日志记录是否有必要等等，从而对测试日志进行精简。</p><p>比如，对于功能测试函数来说，我们通常没有必要重复执行它，即使是在不同的最大 P 数量下也是如此。注意，这里所说的重复执行指的是，在被测程序的输入（比如说被测函数的参数值）相同情况下的多次执行。</p><p>有些时候，在输入完全相同的情况下，被测程序会因其他外部环境的不同，而表现出不同的行为。这时我们需要考虑的往往应该是：这个程序在设计上是否合理，而不是通过重复执行测试来检测风险。</p><p>还有些时候，我们的程序会无法避免地依赖一些外部环境，比如数据库或者其他服务。这时，我们依然不应该让测试的反复执行成为检测手段，而应该在测试中通过仿造（mock）外部环境，来规避掉它们的不确定性。</p><p>其实，单元测试的意思就是：对单一的功能模块进行边界清晰的测试，并且不掺杂任何对外部环境的检测。这也是“单元”二字要表达的主要含义。</p><p>正好相反，对于性能测试函数来说，我们常常需要反复地执行，并以此试图抹平当时的计算资源调度的细微差别对被测程序性能的影响。通过-cpu标记，我们还能够模拟被测程序在计算能力不同计算机中的性能表现。</p><p>不过要注意，这里设置的最大 P 数量，最好不要超过当前计算机 CPU 核心的实际数量。因为一旦超出计算机实际的并行处理能力，Go 程序在性能上就无法再得到显著地提升了。</p><p>这就像一个漏斗，不论我们怎样灌水，水的漏出速度总是有限的。更何况，为了管理过多的 P，Go 语言运行时系统还会耗费额外的计算资源。</p><p>显然，上述模拟得出的程序性能一定是不准确的。不过，这或多或少可以作为一个参考，因为，这样模拟出的性能一般都会低于程序在计算环境中的实际性能。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：-parallel标记的作用是什么？"><a href="#问题-1：-parallel标记的作用是什么？" class="headerlink" title="问题 1：-parallel标记的作用是什么？"></a>问题 1：-parallel标记的作用是什么？</h4><p>我们在运行go test命令的时候，可以追加标记-parallel，该标记的作用是：设置同一个被测代码包中的功能测试函数的最大并发执行数。该标记的默认值是测试运行时的最大 P 数量（这可以通过调用表达式runtime.GOMAXPROCS(0)获得）。</p><p>我在上篇文章中已经说过，对于功能测试，为了加快测试速度，命令通常会并发地测试多个被测代码包。</p><p>但是，在默认情况下，对于同一个被测代码包中的多个功能测试函数，命令会串行地执行它们。除非我们在一些功能测试函数中显式地调用t.Parallel方法。</p><p>这个时候，这些包含了t.Parallel方法调用的功能测试函数就会被go test命令并发地执行，而并发执行的最大数量正是由-parallel标记值决定的。不过要注意，同一个功能测试函数的多次执行之间一定是串行的。</p><p>你可以运行命令go test -v puzzlers&#x2F;article21&#x2F;q2或者go test -count&#x3D;2 -v puzzlers&#x2F;article21&#x2F;q2，查看测试结果，然后仔细地体会一下。</p><p>最后，强调一下，-parallel标记对性能测试是无效的。当然了，对于性能测试来说，也是可以并发进行的，不过机制上会有所不同。</p><p>概括地讲，这涉及了b.RunParallel方法、b.SetParallelism方法和-cpu标记的联合运用。如果想进一步了解，你可以查看testing代码包的<a href="https://golang.google.cn/pkg/testing/">文档</a>。</p><h4 id="问题-2：性能测试函数中的计时器是做什么用的？"><a href="#问题-2：性能测试函数中的计时器是做什么用的？" class="headerlink" title="问题 2：性能测试函数中的计时器是做什么用的？"></a>问题 2：性能测试函数中的计时器是做什么用的？</h4><p>如果你看过testing包的文档，那么很可能会发现其中的testing.B类型有这么几个指针方法：StartTimer、StopTimer和ResetTimer。这些方法都是用于操作当前的性能测试函数专属的计时器的。</p><p>所谓的计时器，是一个逻辑上的概念，它其实是testing.B类型中一些字段的统称。这些字段用于记录：当前测试函数在当次执行过程中耗费的时间、分配的堆内存的字节数以及分配次数。</p><p>我在下面会以测试函数的执行时间为例，来说明此计时器的用法。不过，你需要知道的是，这三个方法在开始记录、停止记录或重新记录执行时间的同时，也会对堆内存分配字节数和分配次数的记录起到相同的作用。</p><p>实际上，go test命令本身就会用到这样的计时器。当准备执行某个性能测试函数的时候，命令会重置并启动该函数专属的计时器。一旦这个函数执行完毕，命令又会立即停止这个计时器。</p><p>如此一来，命令就能够准确地记录下（我们在前面多次提到的）测试函数执行时间了。然后，命令就会将这个时间与执行时间上限进行比较，并决定是否在改大b.N的值之后，再次执行测试函数。</p><p>还记得吗？这就是我在前面讲过的，对性能测试函数的探索式执行。显然，如果我们在测试函数中自行操作这个计时器，就一定会影响到这个探索式执行的结果。也就是说，这会让命令找到被测程序的最大执行次数有所不同。</p><p>代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// demo.go</span></span><br><span class="line"><span class="keyword">package</span> q3</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;math&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetPrimes 用于获取小于或等于参数max的所有质数。</span></span><br><span class="line"><span class="comment">// 本函数使用的是爱拉托逊斯筛选法（Sieve Of Eratosthenes）。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetPrimes</span><span class="params">(max <span class="type">int</span>)</span></span> []<span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> max &lt;= <span class="number">1</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> []<span class="type">int</span>&#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    marks := <span class="built_in">make</span>([]<span class="type">bool</span>, max)</span><br><span class="line">    <span class="keyword">var</span> count <span class="type">int</span></span><br><span class="line">    squareRoot := <span class="type">int</span>(math.Sqrt(<span class="type">float64</span>(max)))</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">2</span>; i &lt;= squareRoot; i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> marks[i] == <span class="literal">false</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> j := i * i; j &lt; max; j += i &#123;</span><br><span class="line">                <span class="keyword">if</span> marks[j] == <span class="literal">false</span> &#123;</span><br><span class="line">                    marks[j] = <span class="literal">true</span></span><br><span class="line">                    count++</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    primes := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">0</span>, max-count)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">2</span>; i &lt; max; i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> marks[i] == <span class="literal">false</span> &#123;</span><br><span class="line">            primes = <span class="built_in">append</span>(primes, i)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> primes</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//demo_test.go</span></span><br><span class="line"><span class="keyword">package</span> q3</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;testing&quot;</span></span><br><span class="line">    <span class="string">&quot;time&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkGetPrimes</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 你可以注释或者还原下面这四行代码中的第一行和第四行，</span></span><br><span class="line">    <span class="comment">// 并观察测试结果的不同。</span></span><br><span class="line">    b.StopTimer()</span><br><span class="line">    time.Sleep(time.Millisecond * <span class="number">500</span>) <span class="comment">// 模拟某个耗时但与被测程序关系不大的操作。</span></span><br><span class="line">    max := <span class="number">10000</span></span><br><span class="line">    b.StartTimer()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">        GetPrimes(max)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是该函数体中的前四行代码。我先停止了当前测试函数的计时器，然后通过调用time.Sleep函数，模拟了一个比较耗时的额外操作，并且在给变量max赋值之后又启动了该计时器。</p><p>你可以想象一下，我们需要耗费额外的时间去确定max变量的值，虽然在后面它会被传入GetPrimes函数，但是，针对GetPrimes函数本身的性能测试并不应该包含确定参数值的过程。</p><p>因此，我们需要把这个过程所耗费的时间，从当前测试函数的执行时间中去除掉。这样就能够避免这一过程对测试结果的不良影响了。</p><p>每当这个测试函数执行完毕后，go test命令拿到的执行时间都只应该包含调用GetPrimes函数所耗费的那些时间。只有依据这个时间做出的后续判断，以及找到被测程序的最大执行次数才是准确的。</p><p>在性能测试函数中，我们可以通过对b.StartTimer和b.StopTimer方法的联合运用，再去除掉任何一段代码的执行时间。</p><p>相比之下，b.ResetTimer方法的灵活性就要差一些了，它只能用于：去除在调用它之前那些代码的执行时间。不过，无论在调用它的时候，计时器是不是正在运行，它都可以起作用。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-geng-duo-de-ce-shi-shou-fa/</id>
    <link href="http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-geng-duo-de-ce-shi-shou-fa/"/>
    <published>2021-11-23T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>在本篇文章，我会继续为你讲解更多更高级的测试方法。这会涉及testing包中更多的 API、go test命令支持的，更多标记更加复杂的测试结果，以及测试覆盖度分析等等。</p>]]>
    </summary>
    <title>Go语言核心36讲-更多的测试手法</title>
    <updated>2026-04-13T10:24:15.853Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p><strong>人是否会进步以及进步得有多快，依赖的恰恰就是对自我的否定，这包括否定的深刻与否，以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含义。</strong></p><span id="more"></span><h3 id="前导知识：go-程序测试基础知识"><a href="#前导知识：go-程序测试基础知识" class="headerlink" title="前导知识：go 程序测试基础知识"></a>前导知识：go 程序测试基础知识</h3><p>我们来说一下单元测试，它又称程序员测试。顾名思义，这就是程序员们本该做的自我检查工作之一。</p><p>Go 语言的缔造者们从一开始就非常重视程序测试，并且为 Go 程序的开发者们提供了丰富的 API 和工具。利用这些 API 和工具，我们可以创建测试源码文件，并为命令源码文件和库源码文件中的程序实体，编写测试用例。</p><p>在 Go 语言中，一个测试用例往往会由一个或多个测试函数来代表，不过在大多数情况下，每个测试用例仅用一个测试函数就足够了。测试函数往往用于描述和保障某个程序实体的某方面功能，比如，该功能在正常情况下会因什么样的输入，产生什么样的输出，又比如，该功能会在什么情况下报错或表现异常，等等。</p><p>我们可以为 Go 程序编写三类测试，即：功能测试（test）、基准测试（benchmark，也称性能测试），以及示例测试（example）。</p><p>对于前两类测试，从名称上你就应该可以猜到它们的用途。而示例测试严格来讲也是一种功能测试，只不过它更关注程序打印出来的内容。</p><p>一般情况下，一个测试源码文件只会针对于某个命令源码文件，或库源码文件（以下简称被测源码文件）做测试，所以我们总会（并且应该）把它们放在同一个代码包内。</p><p>测试源码文件的主名称应该以被测源码文件的主名称为前导，并且必须以“_test”为后缀。例如，如果被测源码文件的名称为 demo52.go，那么针对它的测试源码文件的名称就应该是 demo52_test.go。</p><p>每个测试源码文件都必须至少包含一个测试函数。并且，从语法上讲，每个测试源码文件中，都可以包含用来做任何一类测试的测试函数，即使把这三类测试函数都塞进去也没有问题。我通常就是这么做的，只要把控好测试函数的分组和数量就可以了。</p><p>我们可以依据这些测试函数针对的不同程序实体，把它们分成不同的逻辑组，并且，利用注释以及帮助类的变量或函数来做分割。同时，我们还可以依据被测源码文件中程序实体的先后顺序，来安排测试源码文件中测试函数的顺序。</p><h3 id="今天的问题是：Go-语言对测试函数的名称和签名都有哪些规定？"><a href="#今天的问题是：Go-语言对测试函数的名称和签名都有哪些规定？" class="headerlink" title="今天的问题是：Go 语言对测试函数的名称和签名都有哪些规定？"></a>今天的问题是：Go 语言对测试函数的名称和签名都有哪些规定？</h3><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><ul><li>对于功能测试函数来说，其名称必须以Test为前缀，并且参数列表中只应有一个*testing.T类型的参数声明。</li><li>对于性能测试函数来说，其名称必须以Benchmark为前缀，并且唯一参数的类型必须是*testing.B类型的。</li><li>对于示例测试函数来说，其名称必须以Example为前缀，但对函数的参数列表没有强制规定。</li></ul><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>问这个问题的目的一般有两个。</p><ul><li>第一个目的当然是考察 Go 程序测试的基本规则。</li><li>第二个目的是作为一个引子，引出第二个问题，即：go test命令执行的主要测试流程是什么？</li></ul><p>我们首先需要记住一点，只有测试源码文件的名称对了，测试函数的名称和签名也对了，当我们运行go test命令的时候，其中的测试代码才有可能被运行。</p><p>go test命令在开始运行时，会先做一些准备工作，比如，确定内部需要用到的命令，检查我们指定的代码包或源码文件的有效性，以及判断我们给予的标记是否合法，等等。</p><p>在准备工作顺利完成之后，go test命令就会针对每个被测代码包，依次地进行构建、执行包中符合要求的测试函数，清理临时文件，打印测试结果。这就是通常情况下的主要测试流程。</p><p>请注意上述的“依次”二字。对于每个被测代码包，go test命令会串行地执行测试流程中的每个步骤。</p><p>但是，为了加快测试速度，它通常会并发地对多个被测代码包进行功能测试，只不过，在最后打印测试结果的时候，它会依照我们给定的顺序逐个进行，这会让我们感觉到它是在完全串行地执行测试流程。</p><p>另一方面，由于并发的测试会让性能测试的结果存在偏差，所以性能测试一般都是串行进行的。更具体地说，只有在所有构建步骤都做完之后，go test命令才会真正地开始进行性能测试。</p><p>并且，下一个代码包性能测试的进行，总会等到上一个代码包性能测试的结果打印完成才会开始，而且性能测试函数的执行也都会是串行的。</p><p>一旦清楚了 Go 程序测试的具体过程，我们的一些疑惑就自然有了答案。比如，那个名叫testIntroduce的测试函数为什么没执行，又比如，为什么即使是简单的性能测试执行起来也会比功能测试慢，等等。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-ce-shi-de-ji-ben-gui-ze-he-liu-cheng-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-ce-shi-de-ji-ben-gui-ze-he-liu-cheng-shang/"/>
    <published>2021-11-23T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p><strong>人是否会进步以及进步得有多快，依赖的恰恰就是对自我的否定，这包括否定的深刻与否，以及否定自我的频率如何。这其实就是“不破不立”这个词表达的含义。</strong></p>]]>
    </summary>
    <title>Go语言核心36讲-测试的基本规则和流程（上）</title>
    <updated>2026-04-13T10:24:15.854Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：怎样解释功能测试的测试结果？"><a href="#问题-1：怎样解释功能测试的测试结果？" class="headerlink" title="问题 1：怎样解释功能测试的测试结果？"></a>问题 1：怎样解释功能测试的测试结果？</h4><p>我们先来看下面的测试命令和结果：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="keyword">go</span> test puzzlers/article20/q2</span><br><span class="line">ok   puzzlers/article20/q2 <span class="number">0.008</span>s</span><br></pre></td></tr></table></figure><p>以$符号开头表明此行展现的是我输入的命令。在这里，我输入了go test puzzlers&#x2F;article20&#x2F;q2，这表示我想对导入路径为puzzlers&#x2F;article20&#x2F;q2的代码包进行测试。代码下面一行就是此次测试的简要结果。</p><span id="more"></span><p>这个简要结果有三块内容。最左边的ok表示此次测试成功，也就是说没有发现测试结果不如预期的情况。</p><p>当然了，这里全由我们编写的测试代码决定，我们总是认定测试代码本身没有 Bug，并且忠诚地落实了我们的测试意图。在测试结果的中间，显示的是被测代码包的导入路径。</p><p>而在最右边，展现的是此次对该代码包的测试所耗费的时间，这里显示的0.008s，即 8 毫秒。不过，当我们紧接着第二次运行这个命令的时候，输出的测试结果会略有不同，如下所示：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="keyword">go</span> test puzzlers/article20/q2</span><br><span class="line">ok   puzzlers/article20/q2 (cached)</span><br></pre></td></tr></table></figure><p>可以看到，结果最右边的不再是测试耗时，而是(cached)。这表明，由于测试代码与被测代码都没有任何变动，所以go test命令直接把之前缓存测试成功的结果打印出来了。</p><p>go 命令通常会缓存程序构建的结果，以便在将来的构建中重用。我们可以通过运行go env GOCACHE命令来查看缓存目录的路径。缓存的数据总是能够正确地反映出当时的各种源码文件、构建环境、编译器选项等等的真实情况。</p><p>一旦有任何变动，缓存数据就会失效，go 命令就会再次真正地执行操作。所以我们并不用担心打印出的缓存数据不是实时的结果。go 命令会定期地删除最近未使用的缓存数据，但是，如果你想手动删除所有的缓存数据，运行一下go clean -cache命令就好了。</p><p>对于测试成功的结果，go 命令也是会缓存的。运行go clean -testcache将会删除所有的测试结果缓存。不过，这样做肯定不会删除任何构建结果缓存。</p><blockquote><p>此外，设置环境变量GODEBUG的值也可以稍稍地改变 go 命令的缓存行为。比如，设置值为gocacheverify&#x3D;1将会导致 go 命令绕过任何的缓存数据，而真正地执行操作并重新生成所有结果，然后再去检查新的结果与现有的缓存数据是否一致。</p></blockquote><p>总之，我们并不用在意缓存数据的存在，因为它们肯定不会妨碍go test命令打印正确的测试结果。</p><p>你可能会问，如果测试失败，命令打印的结果将会是怎样的？如果功能测试函数的那个唯一参数被命名为t，那么当我们在其中调用t.Fail方法时，虽然当前的测试函数会继续执行下去，但是结果会显示该测试失败。如下所示：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="keyword">go</span> test puzzlers/article20/q2</span><br><span class="line">--- FAIL: TestFail (<span class="number">0.00</span>s)</span><br><span class="line"> demo53_test.<span class="keyword">go</span>:<span class="number">49</span>: Failed.</span><br><span class="line">FAIL</span><br><span class="line">FAIL puzzlers/article20/q2 <span class="number">0.007</span>s</span><br></pre></td></tr></table></figure><p>我们运行的命令与之前是相同的，但是我新增了一个功能测试函数TestFail，并在其中调用了t.Fail方法。测试结果显示，对被测代码包的测试，由于TestFail函数的测试失败而宣告失败。</p><p>注意，对于失败测试的结果，go test命令并不会进行缓存，所以，这种情况下的每次测试都会产生全新的结果。另外，如果测试失败了，那么go test命令将会导致：失败的测试函数中的常规测试日志一并被打印出来。</p><p>在这里的测试结果中，之所以显示了“demo53_test.go:49: Failed.”这一行，是因为我在TestFail函数中的调用表达式t.Fail()的下边编写了代码t.Log(“Failed.”)。</p><p>t.Log方法以及t.Logf方法的作用，就是打印常规的测试日志，只不过当测试成功的时候，go test命令就不会打印这类日志了。如果你想在测试结果中看到所有的常规测试日志，那么可以在运行go test命令的时候加入标记-v。</p><blockquote><p>若我们想让某个测试函数在执行的过程中立即失败，则可以在该函数中调用t.FailNow方法。<br>我在下面把TestFail函数中的t.Fail()改为t.FailNow()。<br>与t.Fail()不同，在t.FailNow()执行之后，当前函数会立即终止执行。换句话说，该行代码之后的所有代码都会失去执行机会。在这样修改之后，我再次运行上面的命令，得到的结果如下</p></blockquote><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">--- FAIL: TestFail (<span class="number">0.00</span>s)</span><br><span class="line">FAIL</span><br><span class="line">FAIL puzzlers/article20/q2 <span class="number">0.008</span>s</span><br></pre></td></tr></table></figure><blockquote><p>显然，之前显示在结果中的常规测试日志并没有出现在这里。</p></blockquote><p>顺便说一下，如果你想在测试失败的同时打印失败测试日志，那么可以直接调用t.Error方法或者t.Errorf方法。</p><p>前者相当于t.Log方法和t.Fail方法的连续调用，而后者也与之类似，只不过它相当于先调用了t.Logf方法。</p><p>除此之外，还有t.Fatal方法和t.Fatalf方法，它们的作用是在打印失败错误日志之后立即终止当前测试函数的执行并宣告测试失败。更具体地说，这相当于它们在最后都调用了t.FailNow方法。</p><h4 id="问题-2：怎样解释性能测试的测试结果？"><a href="#问题-2：怎样解释性能测试的测试结果？" class="headerlink" title="问题 2：怎样解释性能测试的测试结果？"></a>问题 2：怎样解释性能测试的测试结果？</h4><p>性能测试与功能测试的结果格式有很多相似的地方。我们在这里仅关注前者的特殊之处。请看下面的打印结果。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="keyword">go</span> test -bench=. -run=^$ puzzlers/article20/q3</span><br><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: puzzlers/article20/q3</span><br><span class="line">BenchmarkGetPrimes<span class="number">-8</span>      <span class="number">500000</span>       <span class="number">2314</span> ns/op</span><br><span class="line">PASS</span><br><span class="line">ok   puzzlers/article20/q3 <span class="number">1.192</span>s</span><br></pre></td></tr></table></figure><p>我在运行go test命令的时候加了两个标记。第一个标记及其值为-bench&#x3D;.，只有有了这个标记，命令才会进行性能测试。该标记的值.表明需要执行任意名称的性能测试函数，当然了，函数名称还是要符合 Go 程序测试的基本规则的。</p><p>第二个标记及其值是-run&#x3D;^$，这个标记用于表明需要执行哪些功能测试函数，这同样也是以函数名称为依据的。该标记的值^$意味着：只执行名称为空的功能测试函数，换句话说，不执行任何功能测试函数。</p><p>你可能已经看出来了，这两个标记的值都是正则表达式。实际上，它们只能以正则表达式为值。此外，如果运行go test命令的时候不加-run标记，那么就会使它执行被测代码包中的所有功能测试函数。</p><p>再来看测试结果，重点说一下倒数第三行的内容。BenchmarkGetPrimes-8被称为单个性能测试的名称，它表示命令执行了性能测试函数BenchmarkGetPrimes，并且当时所用的最大 P 数量为8。</p><p>最大 P 数量相当于可以同时运行 goroutine 的逻辑 CPU 的最大个数。这里的逻辑 CPU，也可以被称为 CPU 核心，但它并不等同于计算机中真正的 CPU 核心，只是 Go 语言运行时系统内部的一个概念，代表着它同时运行 goroutine 的能力。</p><p>顺便说一句，一台计算机的 CPU 核心的个数，意味着它能在同一时刻执行多少条程序指令，代表着它并行处理程序指令的能力。</p><p>我们可以通过调用 runtime.GOMAXPROCS函数改变最大 P 数量，也可以在运行go test命令时，加入标记-cpu来设置一个最大 P 数量的列表，以供命令在多次测试时使用。</p><p>在性能测试名称右边的是，go test命令最后一次执行性能测试函数（即BenchmarkGetPrimes函数）的时候，被测函数（即GetPrimes函数）被执行的实际次数。这是什么意思呢？</p><p>go test命令在执行性能测试函数的时候会给它一个正整数，若该测试函数的唯一参数的名称为b，则该正整数就由b.N代表。我们应该在测试函数中配合着编写代码，比如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line"> GetPrimes(<span class="number">1000</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我在一个会迭代b.N次的循环中调用了GetPrimes函数，并给予它参数值1000。go test命令会先尝试把b.N设置为1，然后执行测试函数。</p><p>如果测试函数的执行时间没有超过上限，此上限默认为 1 秒，那么命令就会改大b.N的值，然后再次执行测试函数，如此往复，直到这个时间大于或等于上限为止。</p><p>当某次执行的时间大于或等于上限时，我们就说这是命令此次对该测试函数的最后一次执行。这时的b.N的值就会被包含在测试结果中，也就是上述测试结果中的500000。</p><p>我们可以简称该值为执行次数，但要注意，它指的是被测函数的执行次数，而不是性能测试函数的执行次数。</p><p>最后再看这个执行次数的右边，2314 ns&#x2F;op表明单次执行GetPrimes函数的平均耗时为2314纳秒。这其实就是通过将最后一次执行测试函数时的执行时间，除以（被测函数的）执行次数而得出的。</p><p><img src="https://static001.geekbang.org/resource/image/78/69/78d4c73a9aa9d48b59d3fd304d4b2069.png" alt="性能测试结果的基本解读"></p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-ce-shi-de-ji-ben-gui-ze-he-liu-cheng-xia/</id>
    <link href="http://blog.chcaty.cn/2021/11/23/go-yu-yan-he-xin-36-jiang-ce-shi-de-ji-ben-gui-ze-he-liu-cheng-xia/"/>
    <published>2021-11-23T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：怎样解释功能测试的测试结果？"><a href="#问题-1：怎样解释功能测试的测试结果？" class="headerlink" title="问题 1：怎样解释功能测试的测试结果？"></a>问题 1：怎样解释功能测试的测试结果？</h4><p>我们先来看下面的测试命令和结果：</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="keyword">go</span> test puzzlers/article20/q2</span><br><span class="line">ok   puzzlers/article20/q2 <span class="number">0.008</span>s</span><br></pre></td></tr></table></figure>

<p>以$符号开头表明此行展现的是我输入的命令。在这里，我输入了go test puzzlers&#x2F;article20&#x2F;q2，这表示我想对导入路径为puzzlers&#x2F;article20&#x2F;q2的代码包进行测试。代码下面一行就是此次测试的简要结果。</p>]]>
    </summary>
    <title>Go语言核心36讲-测试的基本规则和流程（下）</title>
    <updated>2026-04-13T10:24:15.854Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>panic 之中可以包含一个值，用于简要解释引发此 panic 的原因。</p><p>如果一个 panic 是我们在无意间引发的，那么其中的值只能由 Go 语言运行时系统给定。但是，当我们使用panic函数有意地引发一个 panic 的时候，却可以自行指定其包含的值。我们今天的第一个问题就是针对后一种情况提出的。</p><span id="more"></span><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：怎样让-panic-包含一个值，以及应该让它包含什么样的值？"><a href="#问题-1：怎样让-panic-包含一个值，以及应该让它包含什么样的值？" class="headerlink" title="问题 1：怎样让 panic 包含一个值，以及应该让它包含什么样的值？"></a>问题 1：怎样让 panic 包含一个值，以及应该让它包含什么样的值？</h4><p>这其实很简单，在调用panic函数时，把某个值作为参数传给该函数就可以了。由于panic函数的唯一一个参数是空接口（也就是interface{}）类型的，所以从语法上讲，它可以接受任何类型的值。</p><p>但是，我们最好传入error类型的错误值，或者其他的可以被有效序列化的值。这里的“有效序列化”指的是，可以更易读地去表示形式转换。</p><p>还记得吗？对于fmt包下的各种打印函数来说，error类型值的Error方法与其他类型值的String方法是等价的，它们的唯一结果都是string类型的。</p><p>我们在通过占位符%s打印这些值的时候，它们的字符串表示形式分别都是这两种方法产出的。</p><p>一旦程序异常了，我们就一定要把异常的相关信息记录下来，这通常都是记到程序日志里。</p><p>我们在为程序排查错误的时候，首先要做的就是查看和解读程序日志；而最常用也是最方便的日志记录方式，就是记下相关值的字符串表示形式。</p><p>所以，如果你觉得某个值有可能会被记到日志里，那么就应该为它关联String方法。如果这个值是error类型的，那么让它的Error方法返回你为它定制的字符串表示形式就可以了。</p><p>对于此，你可能会想到fmt.Sprintf，以及fmt.Fprintf这类可以格式化并输出参数的函数。</p><p>是的，它们本身就可以被用来输出值的某种表示形式。不过，它们在功能上，肯定远不如我们自己定义的Error方法或者String方法。因此，为不同的数据类型分别编写这两种方法总是首选。</p><p>可是，这与传给panic函数的参数值又有什么关系呢？其实道理是相同的。至少在程序崩溃的时候，panic 包含的那个值字符串表示形式会被打印出来。</p><p>另外，我们还可以施加某种保护措施，避免程序的崩溃。这个时候，panic 包含的值会被取出，而在取出之后，它一般都会被打印出来或者记录到日志里。</p><p>既然说到了应对 panic 的保护措施，我们再来看下面一个问题。</p><h4 id="问题-2：怎样施加应对-panic-的保护措施，从而避免程序崩溃？"><a href="#问题-2：怎样施加应对-panic-的保护措施，从而避免程序崩溃？" class="headerlink" title="问题 2：怎样施加应对 panic 的保护措施，从而避免程序崩溃？"></a>问题 2：怎样施加应对 panic 的保护措施，从而避免程序崩溃？</h4><p>Go 语言的内建函数recover专用于恢复 panic，或者说平息运行时恐慌。recover函数无需任何参数，并且会返回一个空接口类型的值。</p><p>如果用法正确，这个值实际上就是即将恢复的 panic 包含的值。并且，如果这个 panic 是因我们调用panic函数而引发的，那么该值同时也会是我们此次调用panic函数时，传入的参数值副本。请注意，这里强调用法的正确。我们先来看看什么是不正确的用法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"> <span class="string">&quot;errors&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"> fmt.Println(<span class="string">&quot;Enter function main.&quot;</span>)</span><br><span class="line"> <span class="comment">// 引发panic。</span></span><br><span class="line"> <span class="built_in">panic</span>(errors.New(<span class="string">&quot;something wrong&quot;</span>))</span><br><span class="line"> p := <span class="built_in">recover</span>()</span><br><span class="line"> fmt.Printf(<span class="string">&quot;panic: %s\n&quot;</span>, p)</span><br><span class="line"> fmt.Println(<span class="string">&quot;Exit function main.&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上面这个main函数中，我先通过调用panic函数引发了一个 panic，紧接着想通过调用recover函数恢复这个 panic。可结果呢？你一试便知，程序依然会崩溃，这个recover函数调用并不会起到任何作用，甚至都没有机会执行。</p><p>还记得吗？我提到过 panic 一旦发生，控制权就会讯速地沿着调用栈的反方向传播。所以，在panic函数调用之后的代码，根本就没有执行的机会。</p><p>那如果我把调用recover函数的代码提前呢？也就是说，先调用recover函数，再调用panic函数会怎么样呢？</p><p>这显然也是不行的，因为，如果在我们调用recover函数时未发生 panic，那么该函数就不会做任何事情，并且只会返回一个nil。</p><p>换句话说，这样做毫无意义。那么，到底什么才是正确的recover函数用法呢？这就不得不提到defer语句了。</p><p>顾名思义，defer语句就是被用来延迟执行代码的。延迟到什么时候呢？这要延迟到该语句所在的函数即将执行结束的那一刻，无论结束执行的原因是什么。</p><p>这与go语句有些类似，一个defer语句总是由一个defer关键字和一个调用表达式组成。</p><p>这里存在一些限制，有一些调用表达式是不能出现在这里的，包括：针对 Go 语言内建函数的调用表达式，以及针对unsafe包中的函数的调用表达式。</p><p>顺便说一下，对于go语句中的调用表达式，限制也是一样的。另外，在这里被调用的函数可以是有名称的，也可以是匿名的。我们可以把这里的函数叫做defer函数或者延迟函数。注意，被延迟执行的是defer函数，而不是defer语句。</p><p>我刚才说了，无论函数结束执行的原因是什么，其中的defer函数调用都会在它即将结束执行的那一刻执行。即使导致它执行结束的原因是一个 panic 也会是这样。正因为如此，我们需要联用defer语句和recover函数调用，才能够恢复一个已经发生的 panic。</p><p>修正后的代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"> <span class="string">&quot;errors&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"> fmt.Println(<span class="string">&quot;Enter function main.&quot;</span>)</span><br><span class="line"> <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Enter defer function.&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> p := <span class="built_in">recover</span>(); p != <span class="literal">nil</span> &#123;</span><br><span class="line">   fmt.Printf(<span class="string">&quot;panic: %s\n&quot;</span>, p)</span><br><span class="line">  &#125;</span><br><span class="line">  fmt.Println(<span class="string">&quot;Exit defer function.&quot;</span>)</span><br><span class="line"> &#125;()</span><br><span class="line"> <span class="comment">// 引发panic。</span></span><br><span class="line"> <span class="built_in">panic</span>(errors.New(<span class="string">&quot;something wrong&quot;</span>))</span><br><span class="line"> fmt.Println(<span class="string">&quot;Exit function main.&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个main函数中，我先编写了一条defer语句，并在defer函数中调用了recover函数。仅当调用的结果值不为nil时，也就是说只有 panic 确实已发生时，我才会打印一行以“panic:”为前缀的内容。</p><p>紧接着，我调用了panic函数，并传入了一个error类型值。这里一定要注意，我们要尽量把defer语句写在函数体的开始处，因为在引发 panic 的语句之后的所有语句，都不会有任何执行机会。</p><p>也只有这样，defer函数中的recover函数调用才会拦截，并恢复defer语句所属的函数，及其调用的代码中发生的所有 panic。</p><h4 id="问题-3：如果一个函数中有多条defer语句，那么那几个defer函数调用的执行顺序是怎样的？"><a href="#问题-3：如果一个函数中有多条defer语句，那么那几个defer函数调用的执行顺序是怎样的？" class="headerlink" title="问题 3：如果一个函数中有多条defer语句，那么那几个defer函数调用的执行顺序是怎样的？"></a>问题 3：如果一个函数中有多条defer语句，那么那几个defer函数调用的执行顺序是怎样的？</h4><p>如果只用一句话回答的话，那就是：在同一个函数中，defer函数调用的执行顺序与它们分别所属的defer语句的出现顺序（更严谨地说，是执行顺序）完全相反。</p><p>当一个函数即将结束执行时，其中的写在最下边的defer函数调用会最先执行，其次是写在它上边、与它的距离最近的那个defer函数调用，以此类推，最上边的defer函数调用会最后一个执行。</p><p>如果函数中有一条for语句，并且这条for语句中包含了一条defer语句，那么，显然这条defer语句的执行次数，就取决于for语句的迭代次数。</p><p>并且，同一条defer语句每被执行一次，其中的defer函数调用就会产生一次，而且，这些函数调用同样不会被立即执行。</p><p>那么问题来了，这条for语句中产生的多个defer函数调用，会以怎样的顺序执行呢？</p><p>为了彻底搞清楚，我们需要弄明白defer语句执行时发生的事情。</p><p>其实也并不复杂，在defer语句每次执行的时候，Go 语言会把它携带的defer函数及其参数值另行存储到一个链表中。</p><p>这个链表与该defer语句所属的函数是对应的，并且，它是先进后出（FILO）的，相当于一个栈。</p><p>在需要执行某个函数中的defer函数调用的时候，Go 语言会先拿到对应的链表，然后从该链表中一个一个地取出defer函数及其参数值，并逐个执行调用。</p><p>这正是我说“defer函数调用与其所属的defer语句的执行顺序完全相反”的原因了。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/22/go-yu-yan-he-xin-36-jiang-panic-han-shu-recover-han-shu-yi-ji-defer-yu-ju-xia/</id>
    <link href="http://blog.chcaty.cn/2021/11/22/go-yu-yan-he-xin-36-jiang-panic-han-shu-recover-han-shu-yi-ji-defer-yu-ju-xia/"/>
    <published>2021-11-22T15:43:58.000Z</published>
    <summary>
      <![CDATA[<p>panic 之中可以包含一个值，用于简要解释引发此 panic 的原因。</p>
<p>如果一个 panic 是我们在无意间引发的，那么其中的值只能由 Go 语言运行时系统给定。但是，当我们使用panic函数有意地引发一个 panic 的时候，却可以自行指定其包含的值。我们今天的第一个问题就是针对后一种情况提出的。</p>]]>
    </summary>
    <title>Go语言核心36讲-panic函数、recover函数以及defer语句（下）</title>
    <updated>2026-04-13T10:24:15.850Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题：怎样根据实际情况给予恰当的错误值？"><a href="#问题：怎样根据实际情况给予恰当的错误值？" class="headerlink" title="问题：怎样根据实际情况给予恰当的错误值？"></a>问题：怎样根据实际情况给予恰当的错误值？</h4><p>我们已经知道，构建错误值体系的基本方式有两种，即：创建立体的错误类型体系和创建扁平的错误值列表。</p><p>先说错误类型体系。由于在 Go 语言中实现接口是非侵入式的，所以我们可以做得很灵活。比如，在标准库的net代码包中，有一个名为Error的接口类型。它算是内建接口类型error的一个扩展接口，因为error是net.Error的嵌入接口。</p><p>net.Error接口除了拥有error接口的Error方法之外，还有两个自己声明的方法：Timeout和Temporary。</p><span id="more"></span><p>net包中有很多错误类型都实现了net.Error接口，比如：</p><ol><li>*net.OpError；</li><li>*net.AddrError；</li><li>net.UnknownNetworkError等等。</li></ol><p>你可以把这些错误类型想象成一棵树，内建接口error就是树的根，而net.Error接口就是一个在根上延伸的第一级非叶子节点。</p><p>同时，你也可以把这看做是一种多层分类的手段。当net包的使用者拿到一个错误值的时候，可以先判断它是否是net.Error类型的，也就是说该值是否代表了一个网络相关的错误。</p><p>如果是，那么我们还可以再进一步判断它的类型是哪一个更具体的错误类型，这样就能知道这个网络相关的错误具体是由于操作不当引起的，还是因为网络地址问题引起的，又或是由于网络协议不正确引起的。</p><p>当我们细看net包中的这些具体错误类型的实现时，还会发现，与os包中的一些错误类型类似，它们也都有一个名为Err、类型为error接口类型的字段，代表的也是当前错误的潜在错误。</p><p>所以说，这些错误类型的值之间还可以有另外一种关系，即：链式关系。比如说，使用者调用net.DialTCP之类的函数时，net包中的代码可能会返回给他一个*net.OpError类型的错误值，以表示由于他的操作不当造成了一个错误。</p><p>同时，这些代码还可能会把一个*net.AddrError或net.UnknownNetworkError类型的值赋给该错误值的Err字段，以表明导致这个错误的潜在原因。如果，此处的潜在错误值的Err字段也有非nil的值，那么将会指明更深层次的错误原因。如此一级又一级就像链条一样最终会指向问题的根源。</p><p>把以上这些内容总结成一句话就是，用类型建立起树形结构的错误体系，用统一字段建立起可追根溯源的链式错误关联。这是 Go 语言标准库给予我们的优秀范本，非常有借鉴意义。</p><p>不过要注意，如果你不想让包外代码改动你返回的错误值的话，一定要小写其中字段的名称首字母。你可以通过暴露某些方法让包外代码有进一步获取错误信息的权限，比如编写一个可以返回包级私有的err字段值的公开方法Err。</p><p>相比于立体的错误类型体系，扁平的错误值列表就要简单得多了。当我们只是想预先创建一些代表已知错误的错误值时候，用这种扁平化的方式就很恰当了。</p><p>不过，由于error是接口类型，所以通过errors.New函数生成的错误值只能被赋给变量，而不能赋给常量，又由于这些代表错误的变量需要给包外代码使用，所以其访问权限只能是公开的。</p><p>这就带来了一个问题，如果有恶意代码改变了这些公开变量的值，那么程序的功能就必然会受到影响。因为在这种情况下我们往往会通过判等操作来判断拿到的错误值具体是哪一个错误，如果这些公开变量的值被改变了，那么相应的判等操作的结果也会随之改变。</p><p>这里有两个解决方案。第一个方案是，先私有化此类变量，也就是说，让它们的名称首字母变成小写，然后编写公开的用于获取错误值以及用于判等错误值的函数。</p><p>比如，对于错误值os.ErrClosed，先改写它的名称，让其变成os.errClosed，然后再编写ErrClosed函数和IsErrClosed函数。</p><p>当然了，这不是说让你去改动标准库中已有的代码，这样做的危害会很大，甚至是致命的。我只能说，对于你可控的代码，最好还是要尽量收紧访问权限。</p><p>再来说第二个方案，此方案存在于syscall包中。该包中有一个类型叫做Errno，该类型代表了系统调用时可能发生的底层错误。这个错误类型是error接口的实现类型，同时也是对内建类型uintptr的再定义类型。</p><p>由于uintptr可以作为常量的类型，所以syscall.Errno自然也可以。syscall包中声明有大量的Errno类型的常量，每个常量都对应一种系统调用错误。syscall包外的代码可以拿到这些代表错误的常量，但却无法改变它们。</p><p>我们可以仿照这种声明方式来构建我们自己的错误值列表，这样就可以保证错误值的只读特性了。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/22/go-yu-yan-he-xin-36-jiang-cuo-wu-chu-li-xia/</id>
    <link href="http://blog.chcaty.cn/2021/11/22/go-yu-yan-he-xin-36-jiang-cuo-wu-chu-li-xia/"/>
    <published>2021-11-22T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题：怎样根据实际情况给予恰当的错误值？"><a href="#问题：怎样根据实际情况给予恰当的错误值？" class="headerlink" title="问题：怎样根据实际情况给予恰当的错误值？"></a>问题：怎样根据实际情况给予恰当的错误值？</h4><p>我们已经知道，构建错误值体系的基本方式有两种，即：创建立体的错误类型体系和创建扁平的错误值列表。</p>
<p>先说错误类型体系。由于在 Go 语言中实现接口是非侵入式的，所以我们可以做得很灵活。比如，在标准库的net代码包中，有一个名为Error的接口类型。它算是内建接口类型error的一个扩展接口，因为error是net.Error的嵌入接口。</p>
<p>net.Error接口除了拥有error接口的Error方法之外，还有两个自己声明的方法：Timeout和Temporary。</p>]]>
    </summary>
    <title>Go语言核心36讲-错误处理（下）</title>
    <updated>2026-04-13T10:24:15.856Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="今天的问题是：使用携带range子句的for语句时需要注意哪些细节？"><a href="#今天的问题是：使用携带range子句的for语句时需要注意哪些细节？" class="headerlink" title="今天的问题是：使用携带range子句的for语句时需要注意哪些细节？"></a>今天的问题是：使用携带range子句的for语句时需要注意哪些细节？</h3><p>先看下面的代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">numbers1 := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> numbers1 &#123;</span><br><span class="line">  <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">    numbers1[i] |= i</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(numbers1)</span><br></pre></td></tr></table></figure><span id="more"></span><p>我先声明了一个元素类型为int的切片类型的变量numbers1，在该切片中有 6 个元素值，分别是从1到6的整数。我用一条携带range子句的for语句去迭代numbers1变量中的所有元素值。</p><p>在这条for语句中，只有一个迭代变量i。我在每次迭代时，都会先去判断i的值是否等于3，如果结果为true，那么就让numbers1的第i个元素值与i本身做按位或的操作，再把操作结果作为numbers1的新的第i个元素值。最后我会打印出numbers1的值。</p><p>所以具体的问题就是，这段代码执行后会打印出什么内容？</p><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>这里的典型回答是：打印的内容会是[1 2 3 7 5 6]。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>当for语句被执行的时候，在range关键字右边的numbers1会先被求值。</p><p>这个位置上的代码被称为range表达式。range表达式的结果值可以是数组、数组的指针、切片、字符串、字典或者允许接收操作的通道中的某一个，并且结果值只能有一个。</p><p>对于不同种类的range表达式结果值，for语句的迭代变量的数量可以有所不同。</p><p>就拿这里的numbers1来说，它是一个切片，那么迭代变量就可以有两个，右边的迭代变量代表当次迭代对应的某一个元素值，而左边的迭代变量则代表该元素值在切片中的索引值。</p><p>那么，如果像本题代码中的for语句那样，只有一个迭代变量的情况意味着什么呢？这意味着，该迭代变量只会代表当次迭代对应的元素值的索引值。</p><p>更宽泛地讲，当只有一个迭代变量的时候，数组、数组的指针、切片和字符串的元素值都是无处安放的，我们只能拿到按照从小到大顺序给出的一个个索引值。</p><p>因此，这里的迭代变量i的值会依次是从0到5的整数。当i的值等于3的时候，与之对应的是切片中的第 4 个元素值4。对4和3进行按位或操作得到的结果是7。这就是答案中的第 4 个整数是7的原因了。</p><p><strong>现在，我稍稍修改一下上面的代码。我们再来估算一下打印内容。</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">numbers2 := [...]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line">maxIndex2 := <span class="built_in">len</span>(numbers2) - <span class="number">1</span></span><br><span class="line"><span class="keyword">for</span> i, e := <span class="keyword">range</span> numbers2 &#123;</span><br><span class="line">  <span class="keyword">if</span> i == maxIndex2 &#123;</span><br><span class="line">    numbers2[<span class="number">0</span>] += e</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    numbers2[i+<span class="number">1</span>] += e</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(numbers2)</span><br></pre></td></tr></table></figure><p>注意，我把迭代的对象换成了numbers2。numbers2中的元素值同样是从1到6的 6 个整数，并且元素类型同样是int，但它是一个数组而不是一个切片。</p><p>在for语句中，我总是会对紧挨在当次迭代对应的元素后边的那个元素，进行重新赋值，新的值会是这两个元素的值之和。当迭代到最后一个元素时，我会把此range表达式结果值中的第一个元素值，替换为它的原值与最后一个元素值的和，最后，我会打印出numbers2的值。</p><p><strong>对于这段代码，我的问题依旧是：打印的内容会是什么？你可以先思考一下。</strong></p><p>好了，我要公布答案了。打印的内容会是[7 3 5 7 9 11]。我先来重现一下计算过程。当for语句被执行的时候，在range关键字右边的numbers2会先被求值。这里需要注意两点：</p><ol><li>range表达式只会在for语句开始执行时被求值一次，无论后边会有多少次迭代；</li><li>range表达式的求值结果会被复制，也就是说，被迭代的对象是range表达式结果值的副本而不是原值。</li></ol><p>基于这两个规则，我们接着往下看。在第一次迭代时，我改变的是numbers2的第二个元素的值，新值为3，也就是1和2之和。</p><p>但是，被迭代的对象的第二个元素却没有任何改变，毕竟它与numbers2已经是毫不相关的两个数组了。因此，在第二次迭代时，我会把numbers2的第三个元素的值修改为5，即被迭代对象的第二个元素值2和第三个元素值3的和。</p><p>以此类推，之后的numbers2的元素值依次会是7、9和11。当迭代到最后一个元素时，我会把numbers2的第一个元素的值修改为1和6之和。</p><p>好了，现在该你操刀了。你需要把numbers2的值由一个数组改成一个切片，其中的元素值都不要变。为了避免混淆，你还要把这个切片值赋给变量numbers3，并且把后边代码中所有的numbers2都改为numbers3。</p><p>问题是不变的，执行这段修改版的代码后打印的内容会是什么呢？如果你实在估算不出来，可以先实际执行一下，然后再尝试解释看到的答案。提示一下，切片与数组是不同的，前者是引用类型的，而后者是值类型的。</p><h3 id="知识扩展"><a href="#知识扩展" class="headerlink" title="知识扩展"></a>知识扩展</h3><h4 id="问题-1：switch语句中的switch表达式和case表达式之间有着怎样的联系？"><a href="#问题-1：switch语句中的switch表达式和case表达式之间有着怎样的联系？" class="headerlink" title="问题 1：switch语句中的switch表达式和case表达式之间有着怎样的联系？"></a>问题 1：switch语句中的switch表达式和case表达式之间有着怎样的联系？</h4><p>先来看一段代码。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">value1 := [...]<span class="type">int8</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">switch</span> <span class="number">1</span> + <span class="number">3</span> &#123;</span><br><span class="line"><span class="keyword">case</span> value1[<span class="number">0</span>], value1[<span class="number">1</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;0 or 1&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> value1[<span class="number">2</span>], value1[<span class="number">3</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;2 or 3&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> value1[<span class="number">4</span>], value1[<span class="number">5</span>], value1[<span class="number">6</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;4 or 5 or 6&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我先声明了一个数组类型的变量value1，该变量的元素类型是int8。在后边的switch语句中，被夹在switch关键字和左花括号{之间的是1 + 3，这个位置上的代码被称为switch表达式。这个switch语句还包含了三个case子句，而每个case子句又各包含了一个case表达式和一条打印语句。</p><p>所谓的case表达式一般由case关键字和一个表达式列表组成，表达式列表中的多个表达式之间需要有英文逗号,分割，比如，上面代码中的case value1[0], value1[1]就是一个case表达式，其中的两个子表达式都是由索引表达式表示的。</p><p>另外的两个case表达式分别是case value1[2], value1[3]和case value1[4], value1[5], value1[6]。</p><p>此外，在这里的每个case子句中的那些打印语句，会分别打印出不同的内容，这些内容用于表示case子句被选中的原因，比如，打印内容0 or 1表示当前case子句被选中是因为switch表达式的结果值等于0或1中的某一个。另外两条打印语句会分别打印出2 or 3和4 or 5 or 6。</p><p>现在问题来了，拥有这样三个case表达式的switch语句可以成功通过编译吗？如果不可以，原因是什么？如果可以，那么该switch语句被执行后会打印出什么内容。</p><p>我刚才说过，只要switch表达式的结果值与某个case表达式中的任意一个子表达式的结果值相等，该case表达式所属的case子句就会被选中。</p><p>并且，一旦某个case子句被选中，其中的附带在case表达式后边的那些语句就会被执行。与此同时，其他的所有case子句都会被忽略。</p><p>当然了，如果被选中的case子句附带的语句列表中包含了fallthrough语句，那么紧挨在它下边的那个case子句附带的语句也会被执行。</p><p>正因为存在上述判断相等的操作（以下简称判等操作），switch语句对switch表达式的结果类型，以及各个case表达式中子表达式的结果类型都是有要求的。毕竟，在 Go 语言中，只有类型相同的值之间才有可能被允许进行判等操作。</p><p>如果switch表达式的结果值是无类型的常量，比如1 + 3的求值结果就是无类型的常量4，那么这个常量会被自动地转换为此种常量的默认类型的值，比如整数4的默认类型是int，又比如浮点数3.14的默认类型是float64。</p><p>因此，由于上述代码中的switch表达式的结果类型是int，而那些case表达式中子表达式的结果类型却是int8，它们的类型并不相同，所以这条switch语句是无法通过编译的。</p><p>再来看一段很类似的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">value2 := [...]<span class="type">int8</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">switch</span> value2[<span class="number">4</span>] &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="number">0</span>, <span class="number">1</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;0 or 1&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">2</span>, <span class="number">3</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;2 or 3&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;4 or 5 or 6&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中的变量value2与value1的值是完全相同的。但不同的是，我把switch表达式换成了value2[4]，并把下边那三个case表达式分别换为了case 0, 1、case 2, 3和case 4, 5, 6。</p><p>如此一来，switch表达式的结果值是int8类型的，而那些case表达式中子表达式的结果值却是无类型的常量了。这与之前的情况恰恰相反。那么，这样的switch语句可以通过编译吗？</p><p>答案是肯定的。因为，如果case表达式中子表达式的结果值是无类型的常量，那么它的类型会被自动地转换为switch表达式的结果类型，又由于上述那几个整数都可以被转换为int8类型的值，所以对这些表达式的结果值进行判等操作是没有问题的。</p><p>当然了，如果这里说的自动转换没能成功，那么switch语句照样通不过编译。</p><p><img src="https://static001.geekbang.org/resource/image/91/1c/91add0a66b9956f81086285aabc20c1c.png" alt="图"></p><p>通过上面这两道题，你应该可以搞清楚switch表达式和case表达式之间的联系了。由于需要进行判等操作，所以前者和后者中的子表达式的结果类型需要相同。</p><p>switch语句会进行有限的类型转换，但肯定不能保证这种转换可以统一它们的类型。还要注意，如果这些表达式的结果类型有某个接口类型，那么一定要小心检查它们的动态值是否都具有可比性（或者说是否允许判等操作）。</p><p>因为，如果答案是否定的，虽然不会造成编译错误，但是后果会更加严重：引发 panic（也就是运行时恐慌）。</p><h4 id="问题-2：switch语句对它的case表达式有哪些约束？"><a href="#问题-2：switch语句对它的case表达式有哪些约束？" class="headerlink" title="问题 2：switch语句对它的case表达式有哪些约束？"></a>问题 2：switch语句对它的case表达式有哪些约束？</h4><p>那就是：switch语句在case子句的选择上是具有唯一性的。</p><p>正因为如此，switch语句不允许case表达式中的子表达式结果值存在相等的情况，不论这些结果值相等的子表达式，是否存在于不同的case表达式中，都会是这样的结果。具体请看这段代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">value3 := [...]<span class="type">int8</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">switch</span> value3[<span class="number">4</span>] &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;0 or 1 or 2&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;2 or 3 or 4&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;4 or 5 or 6&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>变量value3的值同value1，依然是由从0到6的 7 个整数组成的数组，元素类型是int8。switch表达式是value3[4]，三个case表达式分别是case 0, 1, 2、case 2, 3, 4和case 4, 5, 6。</p><p>由于在这三个case表达式中存在结果值相等的子表达式，所以这个switch语句无法通过编译。不过，好在这个约束本身还有个约束，那就是只针对结果值为常量的子表达式。</p><p>比如，子表达式1+1和2不能同时出现，1+3和4也不能同时出现。有了这个约束的约束，我们就可以想办法绕过这个对子表达式的限制了。再看一段代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">value5 := [...]<span class="type">int8</span>&#123;<span class="number">0</span>, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">switch</span> value5[<span class="number">4</span>] &#123;</span><br><span class="line"><span class="keyword">case</span> value5[<span class="number">0</span>], value5[<span class="number">1</span>], value5[<span class="number">2</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;0 or 1 or 2&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> value5[<span class="number">2</span>], value5[<span class="number">3</span>], value5[<span class="number">4</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;2 or 3 or 4&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> value5[<span class="number">4</span>], value5[<span class="number">5</span>], value5[<span class="number">6</span>]:</span><br><span class="line">  fmt.Println(<span class="string">&quot;4 or 5 or 6&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>变量名换成了value5，但这不是重点。重点是，我把case表达式中的常量都换成了诸如value5[0]这样的索引表达式。</p><p>虽然第一个case表达式和第二个case表达式都包含了value5[2]，并且第二个case表达式和第三个case表达式都包含了value5[4]，但这已经不是问题了。这条switch语句可以成功通过编译。</p><p>不过，这种绕过方式对用于类型判断的switch语句（以下简称为类型switch语句）就无效了。因为类型switch语句中的case表达式的子表达式，都必须直接由类型字面量表示，而无法通过间接的方式表示。代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">value6 := <span class="keyword">interface</span>&#123;&#125;(<span class="type">byte</span>(<span class="number">127</span>))</span><br><span class="line"><span class="keyword">switch</span> t := value6.(<span class="keyword">type</span>) &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="type">uint8</span>, <span class="type">uint16</span>:</span><br><span class="line">  fmt.Println(<span class="string">&quot;uint8 or uint16&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> <span class="type">byte</span>:</span><br><span class="line">  fmt.Printf(<span class="string">&quot;byte&quot;</span>)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">  fmt.Printf(<span class="string">&quot;unsupported type: %T&quot;</span>, t)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>变量value6的值是空接口类型的。该值包装了一个byte类型的值127。我在后面使用类型switch语句来判断value6的实际类型，并打印相应的内容。</p><p>这里有两个普通的case子句，还有一个default case子句。前者的case表达式分别是case uint8, uint16和case byte。你还记得吗？byte类型是uint8类型的别名类型。</p><p>因此，它们两个本质上是同一个类型，只是类型名称不同罢了。在这种情况下，这个类型switch语句是无法通过编译的，因为子表达式byte和uint8重复了。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/11/go-yu-yan-he-xin-36-jiang-if-yu-ju-for-yu-ju-he-switch-yu-ju/</id>
    <link href="http://blog.chcaty.cn/2021/11/11/go-yu-yan-he-xin-36-jiang-if-yu-ju-for-yu-ju-he-switch-yu-ju/"/>
    <published>2021-11-11T15:43:58.000Z</published>
    <summary>
      <![CDATA[<h3 id="今天的问题是：使用携带range子句的for语句时需要注意哪些细节？"><a href="#今天的问题是：使用携带range子句的for语句时需要注意哪些细节？" class="headerlink" title="今天的问题是：使用携带range子句的for语句时需要注意哪些细节？"></a>今天的问题是：使用携带range子句的for语句时需要注意哪些细节？</h3><p>先看下面的代码</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">numbers1 := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>&#125;</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> numbers1 &#123;</span><br><span class="line">  <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">    numbers1[i] |= i</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(numbers1)</span><br></pre></td></tr></table></figure>]]>
    </summary>
    <title>Go语言核心36讲-if语句、for语句和switch语句</title>
    <updated>2026-04-13T10:24:15.850Z</updated>
  </entry>
  <entry>
    <author>
      <name>caty</name>
    </author>
    <category term="编程语言" scheme="http://blog.chcaty.cn/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go语言核心36讲" scheme="http://blog.chcaty.cn/tags/Go%E8%AF%AD%E8%A8%80%E6%A0%B8%E5%BF%8336%E8%AE%B2/"/>
    <category term="编程语言" scheme="http://blog.chcaty.cn/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    <category term="Go" scheme="http://blog.chcaty.cn/tags/Go/"/>
    <content>
      <![CDATA[<blockquote><p>Don’t communicate by sharing memory; share memory by communicating.</p></blockquote><p>从 Go 语言编程的角度解释，这句话的意思就是：不要通过共享数据来通讯，恰恰相反，要以通讯的方式共享数据。</p><span id="more"></span><p>我们已经知道，通道（也就是 channel）类型的值，可以被用来以通讯的方式共享数据。更具体地说，它一般被用来在不同的 goroutine 之间传递数据。那么 goroutine 到底代表着什么呢？</p><p>简单来说，goroutine 代表着并发编程模型中的用户级线程。你可能已经知道，操作系统本身提供了进程和线程，这两种并发执行程序的工具。</p><h3 id="前导内容：进程与线程"><a href="#前导内容：进程与线程" class="headerlink" title="前导内容：进程与线程"></a>前导内容：进程与线程</h3><p>进程，描述的就是程序的执行过程，是运行着的程序的代表。换句话说，一个进程其实就是某个程序运行时的一个产物。如果说静静地躺在那里的代码就是程序的话，那么奔跑着的、正在发挥着既有功能的代码就可以被称为进程。</p><p>我们的电脑为什么可以同时运行那么多应用程序？我们的手机为什么可以有那么多 App 同时在后台刷新？这都是因为在它们的操作系统之上有多个代表着不同应用程序或 App 的进程在同时运行。</p><p>再来说说线程。首先，线程总是在进程之内的，它可以被视为进程中运行着的控制流（或者说代码执行的流程）。</p><p>一个进程至少会包含一个线程。如果一个进程只包含了一个线程，那么它里面的所有代码都只会被串行地执行。每个进程的第一个线程都会随着该进程的启动而被创建，它们可以被称为其所属进程的主线程。</p><p>相对应的，如果一个进程中包含了多个线程，那么其中的代码就可以被并发地执行。除了进程的第一个线程之外，其他的线程都是由进程中已存在的线程创建出来的。</p><p>也就是说，主线程之外的其他线程都只能由代码显式地创建和销毁。这需要我们在编写程序的时候进行手动控制，操作系统以及进程本身并不会帮我们下达这样的指令，它们只会忠实地执行我们的指令。</p><p>不过，在 Go 程序当中，Go 语言的运行时（runtime）系统会帮助我们自动地创建和销毁系统级的线程。这里的系统级线程指的就是我们刚刚说过的操作系统提供的线程。</p><p>而对应的用户级线程指的是架设在系统级线程之上的，由用户（或者说我们编写的程序）完全控制的代码执行流程。用户级线程的创建、销毁、调度、状态变更以及其中的代码和数据都完全需要我们的程序自己去实现和处理。</p><p>这带来了很多优势，比如，因为它们的创建和销毁并不用通过操作系统去做，所以速度会很快，又比如，由于不用等着操作系统去调度它们的运行，所以往往会很容易控制并且可以很灵活。</p><p>但是，劣势也是有的，最明显也最重要的一个劣势就是复杂。如果我们只使用了系统级线程，那么我们只要指明需要新线程执行的代码片段，并且下达创建或销毁线程的指令就好了，其他的一切具体实现都会由操作系统代劳。</p><p>但是，如果使用用户级线程，我们就不得不既是指令下达者，又是指令执行者。我们必须全权负责与用户级线程有关的所有具体实现。</p><p>操作系统不但不会帮忙，还会要求我们的具体实现必须与它正确地对接，否则用户级线程就无法被并发地，甚至正确地运行。毕竟我们编写的所有代码最终都需要通过操作系统才能在计算机上执行。这听起来就很麻烦，不是吗？</p><p><strong>不过别担心，Go 语言不但有着独特的并发编程模型，以及用户级线程 goroutine，还拥有强大的用于调度 goroutine、对接系统级线程的调度器。</strong></p><p>这个调度器是 Go 语言运行时系统的重要组成部分，它主要负责统筹调配 Go 并发编程模型中的三个主要元素，即：G（goroutine 的缩写）、P（processor 的缩写）和 M（machine 的缩写）。</p><p>其中的 M 指代的就是系统级线程。而 P 指的是一种可以承载若干个 G，且能够使这些 G 适时地与 M 进行对接，并得到真正运行的中介。</p><p>从宏观上说，G 和 M 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运行着的 G，需要因某个事件（比如等待 I&#x2F;O 或锁的解除）而暂停运行的时候，调度器总会及时地发现，并把这个 G 与那个 M 分离开，以释放计算资源供那些等待运行的 G 使用。</p><p>而当一个 G 需要恢复运行的时候，调度器又会尽快地为它寻找空闲的计算资源（包括 M）并安排运行。另外，当 M 不够用时，调度器会帮我们向操作系统申请新的系统级线程，而当某个 M 已无用时，调度器又会负责把它及时地销毁掉。</p><p>正因为调度器帮助我们做了很多事，所以我们的 Go 程序才总是能高效地利用操作系统和计算机资源。程序中的所有 goroutine 也都会被充分地调度，其中的代码也都会被并发地运行，即使这样的 goroutine 有数以十万计，也仍然可以如此。</p><p><img src="https://static001.geekbang.org/resource/image/9e/7d/9ea14f68ffbcde373ddb61e186695d7d.png" alt="图"></p><h3 id="今天的问题：什么是主-goroutine，它与我们启用的其他-goroutine-有什么不同？"><a href="#今天的问题：什么是主-goroutine，它与我们启用的其他-goroutine-有什么不同？" class="headerlink" title="今天的问题：什么是主 goroutine，它与我们启用的其他 goroutine 有什么不同？"></a>今天的问题：什么是主 goroutine，它与我们启用的其他 goroutine 有什么不同？</h3><p>先看下面的代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10</span>; i++ &#123;</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">      fmt.Println(i)</span><br><span class="line">    &#125;()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在main函数中写了一条for语句。这条for语句中的代码会迭代运行 10 次，并有一个局部变量i代表着当次迭代的序号，该序号是从0开始的。</p><p>在这条for语句中仅有一条go语句，这条go语句中也仅有一条语句。这条最里面的语句调用了fmt.Println函数并想要打印出变量i的值。这个程序很简单，三条语句逐条嵌套。</p><p>我的具体问题是：这个命令源码文件被执行后会打印出什么内容？</p><h4 id="典型回答"><a href="#典型回答" class="headerlink" title="典型回答"></a>典型回答</h4><p>这道题的典型回答是不会有任何内容被打印出来。</p><h4 id="问题解析"><a href="#问题解析" class="headerlink" title="问题解析"></a>问题解析</h4><p>与一个进程总会有一个主线程类似，每一个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工作完成后被自动地启用，并不需要我们做任何手动的操作。</p><p>想必你已经知道，每条go语句一般都会携带一个函数调用，这个被调用的函数常常被称为go函数。而主 goroutine 的go函数就是那个作为程序入口的main函数。</p><p>一定要注意，go函数真正被执行的时间，总会与其所属的go语句被执行的时间不同。当程序执行到一条go语句的时候，Go 语言的运行时系统，会先试图从某个存放空闲的 G 的队列中获取一个 G（也就是 goroutine），它只有在找不到空闲 G 的情况下才会去创建一个新的 G。</p><p>这也是为什么我总会说“启用”一个 goroutine，而不说“创建”一个 goroutine 的原因。已存在的 goroutine 总是会被优先复用。</p><p>然而，创建 G 的成本也是非常低的。创建一个 G 并不会像新建一个进程或者一个系统级线程那样，必须通过操作系统的系统调用来完成，在 Go 语言的运行时系统内部就可以完全做到了，更何况一个 G 仅相当于为需要并发执行代码片段服务的上下文环境而已。</p><p>在拿到了一个空闲的 G 之后，Go 语言运行时系统会用这个 G 去包装当前的那个go函数（或者说该函数中的那些代码），然后再把这个 G 追加到某个存放可运行的 G 的队列中。</p><p>这类队列中的 G 总是会按照先入先出的顺序，很快地由运行时系统内部的调度器安排运行。虽然这会很快，但是由于上面所说的那些准备工作还是不可避免的，所以耗时还是存在的。</p><p>因此，go函数的执行时间总是会明显滞后于它所属的go语句的执行时间。当然了，这里所说的“明显滞后”是对于计算机的 CPU 时钟和 Go 程序来说的。我们在大多数时候都不会有明显的感觉。</p><p>在说明了原理之后，我们再来看这种原理下的表象。请记住，只要go语句本身执行完毕，Go 程序完全不会等待go函数的执行，它会立刻去执行后边的语句。这就是所谓的异步并发地执行。</p><p>这里“后边的语句”指的一般是for语句中的下一个迭代。然而，当最后一个迭代运行的时候，这个“后边的语句”是不存在的。</p><p>上面代码中的那条for语句会以很快的速度执行完毕。当它执行完毕时，那 10 个包装了go函数的 goroutine 往往还没有获得运行的机会。</p><p>请注意，go函数中的那个对fmt.Println函数的调用是以for语句中的变量i作为参数的。你可以想象一下，如果当for语句执行完毕的时候，这些go函数都还没有执行，那么它们引用的变量i的值将会是什么？</p><p>它们都会是10，对吗？那么这道题的答案会是“打印出 10 个10”，是这样吗？</p><p>在确定最终的答案之前，你还需要知道一个与主 goroutine 有关的重要特性，即：一旦主 goroutine 中的代码（也就是main函数中的那些代码）执行完毕，当前的 Go 程序就会结束运行。</p><p>如此一来，如果在 Go 程序结束的那一刻，还有 goroutine 未得到运行机会，那么它们就真的没有运行机会了，它们中的代码也就不会被执行了。</p><p>我们刚才谈论过，当for语句的最后一个迭代运行的时候，其中的那条go语句即是最后一条语句。所以，在执行完这条go语句之后，主 goroutine 中的代码也就执行完了，Go 程序会立即结束运行。那么，如果这样的话，还会有任何内容被打印出来吗？</p><p>严谨地讲，Go 语言并不会去保证这些 goroutine 会以怎样的顺序运行。由于主 goroutine 会与我们手动启用的其他 goroutine 一起接受调度，又因为调度器很可能会在 goroutine 中的代码只执行了一部分的时候暂停，以期所有的 goroutine 有更公平的运行机会。</p><p>所以哪个 goroutine 先执行完、哪个 goroutine 后执行完往往是不可预知的，除非我们使用了某种 Go 语言提供的方式进行了人为干预。然而，在这段代码中，我们并没有进行任何人为干预。</p><p>那答案到底是什么呢？就 demo38.go 中如此简单的代码而言，绝大多数情况都会是“不会有任何内容被打印出来”。</p><p>但是为了严谨起见，无论应聘者的回答是“打印出 10 个10”还是“不会有任何内容被打印出来”，又或是“打印出乱序的0到9”，我都会紧接着去追问“为什么？”因为只有你知道了这背后的原理，你做出的回答才会被认为是正确的。</p><p>这个原理是如此的重要，以至于如果你不知道它，那么就几乎无法编写出正确的可并发执行的程序。如果你不知道此原理，那么即使你写的并发程序看起来可以正确地运行，那也肯定是运气好而已。</p>]]>
    </content>
    <id>http://blog.chcaty.cn/2021/11/10/go-yu-yan-he-xin-36-jiang-go-yu-ju-ji-qi-zhi-xing-gui-ze-shang/</id>
    <link href="http://blog.chcaty.cn/2021/11/10/go-yu-yan-he-xin-36-jiang-go-yu-ju-ji-qi-zhi-xing-gui-ze-shang/"/>
    <published>2021-11-10T15:43:58.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>Don’t communicate by sharing memory; share memory by communicating.</p>
</blockquote>
<p>从 Go 语言编程的角度解释，这句话的意思就是：不要通过共享数据来通讯，恰恰相反，要以通讯的方式共享数据。</p>]]>
    </summary>
    <title>Go语言核心36讲-go语句及其执行规则（上）</title>
    <updated>2026-04-13T10:24:15.849Z</updated>
  </entry>
</feed>
