<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Yangjiayu</title>
  
  
  <link href="https://yjyrichard.github.io/atom.xml" rel="self"/>
  
  <link href="https://yjyrichard.github.io/"/>
  <updated>2026-04-09T15:19:02.203Z</updated>
  <id>https://yjyrichard.github.io/</id>
  
  <author>
    <name>Yangjiayu</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>LLM时代为什么一定要补数学基础：从导数、向量到深度学习与大模型</title>
    <link href="https://yjyrichard.github.io/posts/2de3af35.html"/>
    <id>https://yjyrichard.github.io/posts/2de3af35.html</id>
    <published>2026-04-09T15:06:28.776Z</published>
    <updated>2026-04-09T15:19:02.203Z</updated>
    
    <content type="html"><![CDATA[<h1>LLM时代为什么一定要补数学基础：从导数、向量到深度学习与大模型</h1><p>很多人刚接触大模型时，都会有一个很自然的想法：</p><p>现在各种 <code>AI API</code>、开源模型、智能体框架都这么成熟了，我还需要学数学吗？</p><p>如果你的目标只是“会调用一个模型”，那数学确实不是第一优先级。<br>但如果你的目标是更进一步，真正理解下面这些问题：</p><ul><li>模型为什么能训练出来？</li><li>损失函数到底在损失什么？</li><li>为什么参数一更新，效果就变了？</li><li>为什么大家天天在说向量、矩阵、张量、概率分布？</li><li>为什么大模型的本质是“预测下一个 token 的概率”？</li></ul><p>那你就绕不开数学。</p><p>而且，到了今天这个 <code>LLM</code> 时代，数学不是变得不重要了，反而变得更重要了。因为模型越来越强，系统越来越复杂，越往后走，你越会发现：<strong>那些看似高深的模型原理，最后都能落回到几块基础数学上。</strong></p><p>这篇文章不是一份“考试型”数学笔记，也不是一篇只讲概念不落地的科普。<br>我想做的是一件更实用的事：</p><p><strong>带你从零建立一张认知地图，弄明白微积分、线性代数、概率统计为什么是机器学习、深度学习和大模型的底层语言。</strong></p><p>如果你是小白，不用怕。你不需要一开始就会推导 Transformer 全部公式。<br>你需要先建立直觉，知道每个公式到底在描述什么、解决什么问题、最终如何落到模型训练上。</p><hr><h2 id="一、先记住一条主线：模型到底在做什么">一、先记住一条主线：模型到底在做什么</h2><p>不管是传统机器学习、神经网络，还是今天的大模型，它们本质上都在做三件事：</p><ol><li><strong>表示信息</strong></li><li><strong>计算分数或概率</strong></li><li><strong>根据误差不断调整参数</strong></li></ol><p>换句话说，模型不是“凭空变聪明”的，它是通过大量数据，一点点把参数调到更合适的位置。</p><p>这背后分别对应着三类数学：</p><table><thead><tr><th>数学基础</th><th>在 AI 中对应什么</th><th>解决什么问题</th></tr></thead><tbody><tr><td>微积分</td><td>导数、偏导、梯度、链式法则</td><td>参数怎么更新，模型怎么学习</td></tr><tr><td>线性代数</td><td>向量、矩阵、张量、内积</td><td>数据怎么表示，计算怎么高效进行</td></tr><tr><td>概率统计</td><td>概率分布、条件概率、似然、交叉熵</td><td>模型如何描述不确定性，如何做预测</td></tr></tbody></table><p>如果把 AI 模型比作一个不断被训练的“超级函数”，那么：</p><ul><li>微积分告诉我们，这个函数该往哪个方向调</li><li>线性代数告诉我们，这个函数内部怎么高效算</li><li>概率统计告诉我们，这个函数输出的结果该如何解释</li></ul><p>你可以把这篇文章理解成一场“拆机”。</p><p>我们不是为了学数学而学数学，而是为了拆开机器学习和大模型的外壳，看看里面的发动机到底是怎么运转的。</p><hr><h2 id="二、微积分：模型为什么能“学会”">二、微积分：模型为什么能“学会”</h2><p>很多人第一次听到“导数”“梯度下降”这些词时，会觉得这是纯数学家的世界。<br>实际上，微积分在 AI 里的角色很朴素：</p><p><strong>模型之所以能学习，本质上是因为我们能衡量“改一点参数，结果会变好还是变坏”。</strong></p><p>而这个“变化率”，就是导数要描述的东西。</p><h3 id="1-导数到底是什么">1. 导数到底是什么</h3><p>先别急着看公式，先看一个生活场景。</p><p>你开车时，速度表上的数字就在告诉你：<br>“此时此刻，位置变化得有多快。”</p><p>如果位置记作 $s(t)$，时间记作 $t$，那么速度就是位置对时间的导数：</p><p>$$<br>v(t) = s’(t) = \frac{ds}{dt}<br>$$</p><p>导数的标准定义是：</p><p>$$<br>f’(x)=\lim_{\Delta x \to 0}\frac{f(x+\Delta x)-f(x)}{\Delta x}<br>$$</p><p>这行公式看起来吓人，但它说的其实是：</p><p><strong>当 $x$ 只变化一点点时，函数值 $f(x)$ 会变化多少。</strong></p><p>也就是说，导数描述的是“局部变化速度”。</p><p>举个最简单的例子：</p><p>$$<br>f(x)=x^2<br>$$</p><p>它的导数是：</p><p>$$<br>f’(x)=2x<br>$$</p><p>这说明什么？</p><ul><li>当 $x=1$ 时，变化率是 $2$</li><li>当 $x=10$ 时，变化率是 $20$</li></ul><p>也就是说，随着 $x$ 变大，函数增长得越来越快。</p><p>这在模型里非常重要。因为训练模型时，我们最关心的一件事就是：</p><p><strong>如果我把某个参数改一点点，损失会往哪个方向变？变化有多剧烈？</strong></p><p>这就是导数第一次登场的地方。</p><h3 id="2-为什么训练模型一定要关心“变化率”">2. 为什么训练模型一定要关心“变化率”</h3><p>假设我们有一个很简单的损失函数：</p><p>$$<br>L(w)=(w-1)^2<br>$$</p><p>这里的 $w$ 是参数，$L(w)$ 是损失。<br>这个函数的最小值显然出现在 $w=1$。</p><p>那模型怎么知道该把 $w$ 调到 1 附近？</p><p>看导数：</p><p>$$<br>\frac{dL}{dw}=2(w-1)<br>$$</p><p>这行式子的信息量很大：</p><ul><li>如果 $w&gt;1$，导数为正，说明继续往右走会让损失更大，所以应该往左走</li><li>如果 $w&lt;1$，导数为负，说明往右走会让损失更小，所以应该往右走</li><li>如果 $w=1$，导数为 0，说明到了一个平衡点</li></ul><p>所以，导数其实就是一个“导航器”。<br>它不直接告诉你答案是什么，但它会告诉你：</p><p><strong>下一步往哪边走，更有可能变好。</strong></p><p>这就是机器学习里最经典的更新思想：</p><p>$$<br>w \leftarrow w - \eta \frac{dL}{dw}<br>$$</p><p>其中：</p><ul><li>$w$ 是参数</li><li>$\eta$ 是学习率，表示每次走多大一步</li><li>$\frac{dL}{dw}$ 是导数，表示当前方向上的变化率</li></ul><p>这条式子就是梯度下降最核心的骨架。</p><h3 id="3-多个参数怎么办：偏导和梯度来了">3. 多个参数怎么办：偏导和梯度来了</h3><p>现实中的模型当然不只一个参数。<br>一个线性回归可能有几十个参数，一个神经网络有几百万个参数，一个大模型甚至有几十亿、上百亿参数。</p><p>这时，损失函数就不再是：</p><p>$$<br>L(w)<br>$$</p><p>而更像是：</p><p>$$<br>L(w_1,w_2,\dots,w_n)<br>$$</p><p>也就是说，损失由很多变量共同决定。</p><p>这时候我们就不能只看普通导数了，而要看<strong>偏导数</strong>。</p><p>偏导数的意思是：</p><p><strong>先固定其他变量不动，只看某一个变量变化时，函数会怎么变。</strong></p><p>比如：</p><p>$$<br>f(x,y)=x^2+3y<br>$$</p><p>那么它对 $x$ 的偏导数是：</p><p>$$<br>\frac{\partial f}{\partial x}=2x<br>$$</p><p>对 $y$ 的偏导数是：</p><p>$$<br>\frac{\partial f}{\partial y}=3<br>$$</p><p>如果把所有偏导数收集起来，就得到了<strong>梯度</strong>：</p><p>$$<br>\nabla f(x,y)=<br>\begin{bmatrix}<br>\frac{\partial f}{\partial x}\<br>\frac{\partial f}{\partial y}<br>\end{bmatrix}<br>$$</p><p>梯度可以理解成一个箭头。<br>这个箭头指向函数增长最快的方向。</p><p>那如果我们想让损失下降得最快呢？</p><p>答案很自然：</p><p><strong>朝着梯度的反方向走。</strong></p><p>所以多参数情况下的梯度下降写成：</p><p>$$<br>\mathbf{w} \leftarrow \mathbf{w} - \eta \nabla L(\mathbf{w})<br>$$</p><p>这几乎是整个深度学习训练的灵魂公式。</p><h3 id="4-为什么反向传播离不开链式法则">4. 为什么反向传播离不开链式法则</h3><p>很多人学神经网络，最怕“反向传播”。<br>但如果你先理解链式法则，反向传播就不再神秘。</p><p>链式法则说的是：</p><p>如果 $z$ 依赖于 $y$，而 $y$ 又依赖于 $x$，那么 $z$ 对 $x$ 的变化率，要把中间这层关系乘起来。</p><p>$$<br>\frac{dz}{dx}=\frac{dz}{dy}\cdot\frac{dy}{dx}<br>$$</p><p>这有什么用？</p><p>神经网络本质上就是一层一层函数套起来：</p><p>$$<br>\mathbf{x} \rightarrow \text{线性变换} \rightarrow \text{激活函数} \rightarrow \text{下一层} \rightarrow \text{损失}<br>$$</p><p>输出的误差想传回前面的参数，就必须一级一级往回算。<br>这就是反向传播的本质：</p><p><strong>把误差通过链式法则，从后往前传回去。</strong></p><p>所以你可以把反向传播理解成一句非常朴素的话：</p><blockquote><p>模型最后错了，那前面每一层分别该负多大责任？</p></blockquote><p>而链式法则就是这个“责任分摊系统”。</p><h3 id="5-一个极简代码例子：参数是怎么一步步逼近答案的">5. 一个极简代码例子：参数是怎么一步步逼近答案的</h3><p>下面这段代码没有任何神经网络框架，但它已经把“损失函数 + 导数 + 更新参数”这件事展示出来了：</p><figure class="highlight python"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">loss</span>(<span class="params">w</span>):</span><br><span class="line">    <span class="keyword">return</span> (w - <span class="number">1</span>) ** <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">grad</span>(<span class="params">w</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="number">2</span> * (w - <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">w = <span class="number">5.0</span></span><br><span class="line">lr = <span class="number">0.1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> step <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">10</span>):</span><br><span class="line">    w = w - lr * grad(w)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;step=<span class="subst">&#123;step&#125;</span>, w=<span class="subst">&#123;w:<span class="number">.4</span>f&#125;</span>, loss=<span class="subst">&#123;loss(w):<span class="number">.6</span>f&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><p>这段代码背后发生的事情只有一件：</p><p>模型先看看自己离目标有多远，再根据导数判断该往哪边走，然后一步步把参数推向损失更小的位置。</p><p>如果你能理解这一点，那么你已经理解了神经网络训练最核心的动力机制。</p><hr><h2 id="三、线性代数：为什么-AI-世界里到处都是向量、矩阵、张量">三、线性代数：为什么 AI 世界里到处都是向量、矩阵、张量</h2><p>如果说微积分解决的是“怎么学”，那么线性代数解决的就是“怎么表示”和“怎么算得快”。</p><p>今天的 AI 系统，几乎一切都在向量化：</p><ul><li>一个用户画像可以表示成向量</li><li>一个词可以表示成向量</li><li>一张图片可以表示成矩阵或张量</li><li>一段文本经过编码后会变成一串向量</li></ul><p>所以你会发现，现代模型其实不是在直接处理“文字”“图片”“声音”，而是在处理这些对象对应的数值表示。</p><h3 id="1-标量、向量、矩阵、张量到底是什么">1. 标量、向量、矩阵、张量到底是什么</h3><p>先把这几个概念讲人话。</p><ul><li><strong>标量</strong>：一个数，比如温度 25、分数 0.9</li><li><strong>向量</strong>：一串有顺序的数，比如 $[1, 2, 3]$</li><li><strong>矩阵</strong>：二维表格形式的数据</li><li><strong>张量</strong>：更高维的数据结构，可以理解为“多维数组”</li></ul><p>在 AI 里你会反复看到这些对象，是因为计算机最擅长处理数值，而大量复杂信息都可以编码成数值结构。</p><p>比如一个词“苹果”，模型不会直接理解汉字本身。<br>它通常会先把这个词映射成一个高维向量：</p><p>$$<br>\mathbf{e}_{\text{苹果}} = [0.12, -0.43, 0.78, \dots]<br>$$</p><p>这个向量里的每一维，不一定有肉眼可解释的含义，但整体上它能表达这个词在语义空间中的位置。</p><p>这就是为什么大家常说：</p><p><strong>大模型先把语言变成向量，再在向量空间里做计算。</strong></p><h3 id="2-向量为什么重要：它不只是“几列数字”">2. 向量为什么重要：它不只是“几列数字”</h3><p>向量最重要的作用有两个：</p><ol><li>表示对象</li><li>表示方向</li></ol><p>在机器学习里，一个样本常常就可以表示成一个向量：</p><p>$$<br>\mathbf{x}=<br>\begin{bmatrix}<br>x_1\<br>x_2\<br>\vdots\<br>x_n<br>\end{bmatrix}<br>$$</p><p>这里的每个分量都可以是一个特征，比如：</p><ul><li>用户年龄</li><li>点击次数</li><li>停留时长</li><li>历史购买行为</li></ul><p>在深度学习里，一个词向量、句向量、图像特征向量，本质上也都是这种形式。</p><p>更关键的是，向量不仅能装数据，还能做运算。<br>比如两个向量的<strong>内积</strong>：</p><p>$$<br>\mathbf{x} \cdot \mathbf{w} = \sum_{i=1}^{n} x_i w_i<br>$$</p><p>这在机器学习里非常常见。</p><p>你可以把它理解成：</p><p><strong>把每个特征乘上一个权重，再加起来，得到一个总分。</strong></p><p>这不就是模型打分吗？</p><p>所以很多模型的第一步，本质上都可以写成：</p><p>$$<br>z=\mathbf{w}^T\mathbf{x}+b<br>$$</p><p>其中：</p><ul><li>$\mathbf{x}$ 是输入特征</li><li>$\mathbf{w}$ 是参数权重</li><li>$b$ 是偏置</li><li>$z$ 是打分结果</li></ul><p>线性回归、逻辑回归、神经网络中的线性层，骨架都离不开这一句。</p><h3 id="3-矩阵乘法为什么是深度学习的日常">3. 矩阵乘法为什么是深度学习的日常</h3><p>如果一次只算一个样本，当然可以用向量。<br>但现实中我们会同时处理很多样本，还会有很多神经元一起算。</p><p>这时矩阵就登场了。</p><p>神经网络里最常见的一步是：</p><p>$$<br>\mathbf{y}=W\mathbf{x}+\mathbf{b}<br>$$</p><p>这叫做线性变换。</p><p>它的含义不是“机械地做乘法”，而是：</p><p><strong>把输入向量映射到另一个空间里，形成新的表示。</strong></p><p>为什么这很关键？</p><p>因为深度学习本质上就是不断做“表示变换”。</p><p>比如一句话最开始只是一个个 token，经过 embedding 之后变成向量；<br>经过若干层网络后，这些向量逐渐带上语法、语义、上下文关系；<br>到了最后，模型再根据这些表示去预测下一个 token。</p><p>也就是说，矩阵乘法不是配角，它是主干。</p><h3 id="4-一个简单的-NumPy-例子：线性层到底在算什么">4. 一个简单的 NumPy 例子：线性层到底在算什么</h3><figure class="highlight python"><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">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"></span><br><span class="line">x = np.array([<span class="number">0.8</span>, <span class="number">1.2</span>, -<span class="number">0.5</span>])   <span class="comment"># 输入特征</span></span><br><span class="line">W = np.array([</span><br><span class="line">    [<span class="number">0.2</span>, <span class="number">0.5</span>, -<span class="number">0.1</span>],</span><br><span class="line">    [-<span class="number">0.3</span>, <span class="number">0.7</span>, <span class="number">0.4</span>]</span><br><span class="line">])</span><br><span class="line">b = np.array([<span class="number">0.1</span>, -<span class="number">0.2</span>])</span><br><span class="line"></span><br><span class="line">y = W @ x + b</span><br><span class="line"><span class="built_in">print</span>(y)</span><br></pre></td></tr></table></figure><p>这段代码可以理解为：</p><ul><li>输入向量有 3 个特征</li><li>线性层有 2 个输出单元</li><li>每个输出单元都会看完整个输入向量，并按自己的权重组合出一个新值</li></ul><p>所以，矩阵乘法本质上不是“死算”，而是在做：</p><p><strong>特征重组、信息压缩、方向投影和表示变换。</strong></p><h3 id="5-范数、距离、相似度，为什么在-AI-里这么常见">5. 范数、距离、相似度，为什么在 AI 里这么常见</h3><p>线性代数里还有一类概念也特别重要：长度和距离。</p><p>向量的长度通常用范数来表示。<br>最常见的是 $L_2$ 范数：</p><p>$$<br>|\mathbf{x}|_2=\sqrt{x_1^2+x_2^2+\cdots+x_n^2}<br>$$</p><p>它可以理解成向量在空间中的“几何长度”。</p><p>这有什么用？</p><ul><li>在优化中，我们会限制参数不要太大，防止过拟合</li><li>在推荐系统和检索系统中，我们会比较向量之间是否相似</li><li>在大模型里，我们会用向量相似度去衡量语义接近程度</li></ul><p>比如余弦相似度：</p><p>$$<br>\cos(\theta)=\frac{\mathbf{x}\cdot\mathbf{y}}{|\mathbf{x}||\mathbf{y}|}<br>$$</p><p>如果两个词的向量方向很接近，那么它们在语义上往往也更接近。</p><p>这就是为什么向量数据库、语义检索、Embedding 检索在今天这么重要。</p><p>到了这里，你应该已经能感觉到：</p><p><strong>线性代数不是“AI 里的辅助工具”，它几乎就是 AI 计算的语言本身。</strong></p><hr><h2 id="四、概率统计：模型为什么输出的是“可能性”，不是绝对真理">四、概率统计：模型为什么输出的是“可能性”，不是绝对真理</h2><p>很多初学者会有一个误区：<br>觉得模型应该像计算器一样，只给出一个绝对正确的答案。</p><p>但现实世界不是这么运转的。<br>很多任务本来就带有不确定性。</p><p>比如一句话：</p><blockquote><p>今天的天气真不错，适合去公园里吹吹____。</p></blockquote><p>最后一个词可能是“风”，也可能是“风啊”，也可能是“晚风”。<br>这时候模型不是在算“唯一正确答案”，而是在计算：</p><p><strong>哪个词在当前上下文下更有可能出现。</strong></p><p>这就进入了概率论的世界。</p><h3 id="1-概率到底在-AI-里扮演什么角色">1. 概率到底在 AI 里扮演什么角色</h3><p>概率最朴素的意义，就是衡量一件事发生的可能性。</p><p>如果事件 $A$ 发生的概率记作 $P(A)$，那么：</p><ul><li>$P(A)=1$ 表示一定发生</li><li>$P(A)=0$ 表示不可能发生</li><li>中间的值表示可能性大小</li></ul><p>在机器学习里，模型通常输出的不是“结论本身”，而是一个概率分布。</p><p>例如分类问题里，模型可能给出：</p><p>$$<br>P(\text{猫})=0.8,\quad P(\text{狗})=0.15,\quad P(\text{鸟})=0.05<br>$$</p><p>这表示模型更倾向于认为它是猫，但它并不是在宣告某种绝对真理，而是在表达置信程度。</p><h3 id="2-条件概率：为什么上下文这么重要">2. 条件概率：为什么上下文这么重要</h3><p>大模型最核心的目标之一，是预测下一个 token 的概率。</p><p>它关心的不是某个词单独出现的概率，而是：</p><p><strong>在前文已经给定的情况下，下一个词出现的概率。</strong></p><p>这就是条件概率：</p><p>$$<br>P(A|B)=\frac{P(A \cap B)}{P(B)}<br>$$</p><p>它的读法是：</p><p>“在 $B$ 已经发生的条件下，$A$ 发生的概率是多少。”</p><p>放到语言模型里，就是：</p><p>$$<br>P(x_t \mid x_1,x_2,\dots,x_{t-1})<br>$$</p><p>这行式子可以翻译成一句很接地气的话：</p><blockquote><p>已经看了前面所有词之后，第 $t$ 个词最可能是什么？</p></blockquote><p>所以，LLM 的“理解上下文”并不神秘。<br>至少从训练目标上看，它就是在不断学习条件概率分布。</p><h3 id="3-贝叶斯思维：新证据来了，我们就更新判断">3. 贝叶斯思维：新证据来了，我们就更新判断</h3><p>贝叶斯公式是概率论里特别有“思维味道”的一个公式：</p><p>$$<br>P(A|B)=\frac{P(B|A)P(A)}{P(B)}<br>$$</p><p>它告诉我们：</p><p><strong>当有了新证据之后，我们应该怎样更新原来的判断。</strong></p><p>比如医疗检测里：</p><ul><li>一个病本身很少见</li><li>检测阳性不代表一定患病</li><li>还要综合先验概率、误报率、漏报率一起看</li></ul><p>这套思路在 AI 中也非常重要。</p><p>因为模型不是在真空中思考，它总是在结合已有信息，不断修正对当前问题的判断。</p><p>贝叶斯公式本身不一定直接写在每个大模型代码里，但“根据证据更新信念”这件事，是整个智能系统设计里非常核心的思维方式。</p><h3 id="4-似然函数：不是“结果的概率”，而是“参数解释数据的能力”">4. 似然函数：不是“结果的概率”，而是“参数解释数据的能力”</h3><p>这是很多初学者最容易混淆的概念之一。</p><p>概率通常是：</p><p><strong>已知参数，问数据出现的可能性。</strong></p><p>而似然则反过来：</p><p><strong>已知数据，问哪组参数更能解释这些数据。</strong></p><p>如果模型参数记作 $\theta$，数据记作 $D$，那么似然函数写作：</p><p>$$<br>L(\theta)=P(D|\theta)<br>$$</p><p>注意，这里形式上还是概率，但研究对象已经变了。<br>现在我们把数据看作固定，把参数看作变量。</p><p>这件事为什么关键？</p><p>因为机器学习训练，核心就在做这件事：</p><p><strong>寻找一组参数，让训练数据在这组参数下“看起来最合理”。</strong></p><p>这就是极大似然估计：</p><p>$$<br>\hat{\theta}=\arg\max_{\theta}P(D|\theta)<br>$$</p><p>为了计算方便，我们通常会取对数：</p><p>$$<br>\log L(\theta)=\sum_{i=1}^{n}\log P(x_i|\theta)<br>$$</p><p>把乘法变加法，计算更稳定，也更适合优化。</p><h3 id="5-交叉熵：为什么模型答错时会“疼”">5. 交叉熵：为什么模型答错时会“疼”</h3><p>在深度学习和大模型里，交叉熵几乎是你绕不开的损失函数。</p><p>公式是：</p><p>$$<br>H(p,q)=-\sum_i p_i \log q_i<br>$$</p><p>第一次看这个公式，很多人都会问：</p><p>这到底在算什么？</p><p>你可以先不用从信息论角度理解它，先用训练直觉理解：</p><p><strong>如果真实答案是某一类，而模型给这类的概率很低，那么惩罚就会很大。</strong></p><p>举个例子。<br>真实答案是“猫”：</p><ul><li>如果模型给猫的概率是 $0.9$，损失会比较小</li><li>如果模型给猫的概率是 $0.01$，损失会非常大</li></ul><p>为什么？</p><p>因为：</p><p>$$<br>-\log(0.9)<br>$$</p><p>很小，而：</p><p>$$<br>-\log(0.01)<br>$$</p><p>就很大。</p><p>这意味着交叉熵会逼着模型去做一件事：</p><p><strong>把真实答案的概率抬高。</strong></p><p>而这正是分类模型和语言模型训练中最核心的目标之一。</p><h3 id="6-Softmax：为什么模型最后能输出一个“像概率”的东西">6. Softmax：为什么模型最后能输出一个“像概率”的东西</h3><p>神经网络最后一层常常会输出一组分数，我们把它记作 $z_1,z_2,\dots,z_n$。<br>这些分数本身不是概率，因为它们可能有负数，也不一定加起来等于 1。</p><p>这时就需要 <code>Softmax</code>：</p><p>$$<br>\text{softmax}(z_i)=\frac{e^{z_i}}{\sum_j e^{z_j}}<br>$$</p><p>它的作用是把一组任意分数，变成一组合法的概率分布：</p><ul><li>每一项都大于 0</li><li>所有项加起来等于 1</li></ul><p>所以在分类任务里，<code>Softmax + 交叉熵</code> 是非常经典的一套组合。</p><p>而在 LLM 中，本质上也是类似的事情：</p><p>模型会对整个词表里的 token 打分，再把这些分数变成概率分布，然后从中选择下一个 token。</p><p>你也可以用几行 <code>NumPy</code> 先感受一下这个过程：</p><figure class="highlight python"><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">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"></span><br><span class="line">logits = np.array([<span class="number">2.5</span>, <span class="number">1.0</span>, <span class="number">0.1</span>])   <span class="comment"># 模型对 3 个候选词的原始打分</span></span><br><span class="line">exp_scores = np.exp(logits - logits.<span class="built_in">max</span>())  <span class="comment"># 减去最大值是为了数值稳定</span></span><br><span class="line">probs = exp_scores / exp_scores.<span class="built_in">sum</span>()</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(probs)</span><br></pre></td></tr></table></figure><p>运行后你会得到一组加起来等于 1 的数。<br>这就是模型从“原始分数”走向“概率分布”的最基础过程。</p><p>如果你只记住一句话，那就是：</p><p><strong>大模型并不是“凭感觉写字”，而是在一个巨大的词表上不断做概率分布预测。</strong></p><hr><h2 id="五、从机器学习到深度学习，再到-LLM：这些数学是怎么串起来的">五、从机器学习到深度学习，再到 LLM：这些数学是怎么串起来的</h2><p>前面讲的微积分、线性代数、概率统计，看起来像三门课。<br>但真正到了 AI 里，它们不是分开的，而是紧紧扣在一起的。</p><p>下面我们把这条主线彻底串起来。</p><h3 id="1-传统机器学习：先做人能理解的特征，再让模型学习规律">1. 传统机器学习：先做人能理解的特征，再让模型学习规律</h3><p>早期机器学习更强调“特征工程”。</p><p>比如你要预测房价，可能会手工设计这些特征：</p><ul><li>面积</li><li>地段</li><li>房龄</li><li>楼层</li><li>学区</li></ul><p>然后把它们组成一个向量 $\mathbf{x}$，再让模型学习一个函数：</p><p>$$<br>\hat{y}=f(\mathbf{x})<br>$$</p><p>训练过程通常是：</p><ol><li>用参数对输入打分</li><li>和真实值比较，得到损失</li><li>求导，得到梯度</li><li>更新参数，重复很多轮</li></ol><p>你看，线性代数在表示输入，概率和损失函数在衡量输出是否合理，微积分在负责优化。</p><h3 id="2-深度学习：不再只学答案，还学“表示”">2. 深度学习：不再只学答案，还学“表示”</h3><p>深度学习相比传统机器学习，一个非常关键的变化是：</p><p><strong>模型不仅学习如何预测，还学习如何自动构造中间特征。</strong></p><p>这就是“深”的意义。</p><p>一个简单神经网络可以写成：</p><p>$$<br>\mathbf{h}=\sigma(W_1\mathbf{x}+\mathbf{b}_1)<br>$$</p><p>$$<br>\hat{\mathbf{y}}=W_2\mathbf{h}+\mathbf{b}_2<br>$$</p><p>这里：</p><ul><li>第一层先把输入变成隐藏表示 $\mathbf{h}$</li><li>激活函数 $\sigma$ 提供非线性能力</li><li>第二层再基于隐藏表示做输出</li></ul><p>你可以把它理解成：</p><p>模型不是直接从原始输入跳到答案，而是先学会“怎么理解这个输入”，再做判断。</p><p>层数越多，表示能力通常越强。<br>这也是为什么深度学习能在图像、语音、文本等复杂任务上胜出的原因。</p><h3 id="3-大模型：把“表示学习”推到极致">3. 大模型：把“表示学习”推到极致</h3><p>到了大模型这里，思路又进一步升级了。</p><p>传统模型常常是为一个任务设计的，比如分类、回归、推荐。<br>而大模型做的是一种更通用的事情：</p><p><strong>通过海量文本数据，学习语言世界里的统计规律、语义关系和上下文依赖。</strong></p><p>它的训练目标看起来甚至很简单：</p><p>$$<br>P(x_t \mid x_{&lt;t})<br>$$</p><p>也就是：</p><p>在前文给定的情况下，预测下一个 token。</p><p>如果把整段文本都考虑进去，训练时常见的目标可以写成：</p><p>$$<br>\mathcal{L} = - \sum_{t=1}^{T}\log P(x_t \mid x_{&lt;t})<br>$$</p><p>这行公式特别值得初学者认真看一眼。</p><p>它的意思不是“背下来就完了”，而是：</p><ul><li>对序列中的每一个位置，模型都要预测下一个 token</li><li>如果真实 token 的概率被模型打得很低，这一项损失就会很大</li><li>把整段文本所有位置的损失加起来，就得到了整体训练目标</li></ul><p>所以，LLM 训练从表面上看像是在“学语言”，从数学上看，其实是在不断降低整段文本预测的负对数似然。</p><p>但这件事之所以能变得这么强，是因为：</p><ul><li>数据量极大</li><li>参数量极大</li><li>表示空间极高维</li><li>训练过程把语言中的大量规律都压缩进参数里了</li></ul><p>所以，LLM 的本质不是背答案，而是在高维空间里学习一种复杂的条件概率分布。</p><h3 id="4-为什么-Transformer-会成为主流">4. 为什么 Transformer 会成为主流</h3><p>讲到大模型，绕不开 Transformer。<br>而 Transformer 最核心的一个机制，就是注意力机制：</p><p>$$<br>\text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V<br>$$</p><p>第一次看到这个公式，很多人会直接劝退。<br>但你如果把它翻译成人话，其实没那么可怕。</p><p>它在做的事情大概是：</p><ol><li>用 $Q$ 提出“我当前关心什么”</li><li>用 $K$ 判断“谁和我更相关”</li><li>用 $V$ 提供“真正需要汇总的信息”</li><li>再通过 <code>softmax</code> 把相关性变成权重</li><li>最后做加权求和，得到当前 token 的新表示</li></ol><p>你会发现，这里面几乎每一步都和前面讲的数学直接相关：</p><ul><li>$QK^T$ 是矩阵乘法和内积</li><li><code>softmax</code> 是概率归一化</li><li>权重加和是线性组合</li><li>参数更新靠的是梯度下降和反向传播</li></ul><p>所以所谓“注意力机制很高级”，并不意味着它脱离了基础数学。<br>恰恰相反，它是基础数学在复杂系统中的一次漂亮组合。</p><h3 id="5-为什么说-LLM-的底层逻辑可以浓缩成一句话">5. 为什么说 LLM 的底层逻辑可以浓缩成一句话</h3><p>如果让我用一句话总结大模型训练的核心，我会这么说：</p><p><strong>把文本变成向量，用矩阵做变换，用概率描述下一个 token，用梯度下降让参数不断变好。</strong></p><p>你看，这里面依次出现了：</p><ul><li>向量和矩阵</li><li>概率分布</li><li>损失函数</li><li>导数、梯度、链式法则</li></ul><p>这也正是为什么一旦你理解了基础数学，很多 AI 概念会突然连起来。</p><p>以前你觉得这些词是零散的黑话：</p><ul><li>Embedding</li><li>Attention</li><li>Loss</li><li>Backpropagation</li><li>Softmax</li><li>Cross Entropy</li></ul><p>后来你会发现，它们其实都在讲同一个故事的不同部分。</p><hr><h2 id="六、为什么说“不会数学也能用-AI”，但“想走远必须补数学”">六、为什么说“不会数学也能用 AI”，但“想走远必须补数学”</h2><p>这是一个很现实的话题。</p><p>今天确实有很多人不懂太多数学，也能接 API、做应用、搭工作流、写提示词。<br>这完全没问题，而且也有很高的实际价值。</p><p>但如果你想进入下面这些更深一点的领域：</p><ul><li>看懂模型论文</li><li>理解训练过程和调参逻辑</li><li>自己实现基础模型</li><li>理解为什么模型会过拟合、欠拟合、幻觉</li><li>理解 embedding、检索、排序、重排背后的原理</li><li>做更深入的机器学习、推荐系统、搜索系统和大模型工程</li></ul><p>那数学就会越来越重要。</p><p>因为当你遇到真正复杂的问题时，最后帮你建立判断力的，不是流行词，不是框架名，而是底层原理。</p><p>举几个很典型的场景：</p><ul><li>学习率太大为什么会震荡？这是优化问题。</li><li>为什么参数正则化能缓解过拟合？这和范数、约束有关。</li><li>为什么概率低的正确答案会带来更大的损失？这和对数、交叉熵有关。</li><li>为什么语义检索靠向量相似度就能工作？这和嵌入空间、距离度量有关。</li></ul><p>所以，数学最大的价值，不是让你会做卷子，而是让你在复杂系统面前不慌。</p><hr><h2 id="七、给初学者的一条学习路线：现在该从哪里开始">七、给初学者的一条学习路线：现在该从哪里开始</h2><p>如果你读到这里，已经对“为什么要学数学”有了感觉，那下一步最重要的不是贪多，而是建立顺序。</p><p>我建议你按下面这条路线来：</p><h3 id="第一步：把微积分的核心概念学扎实">第一步：把微积分的核心概念学扎实</h3><p>重点不是所有题型，而是这些概念：</p><ul><li>导数到底是什么</li><li>偏导数是什么意思</li><li>梯度为什么指向增长最快方向</li><li>链式法则为什么能支持反向传播</li><li>梯度下降为什么能优化损失函数</li></ul><p>你学完这一块，再去看“反向传播”“优化器”“学习率”，理解会快很多。</p><h3 id="第二步：把线性代数真正和-AI-场景对应起来">第二步：把线性代数真正和 AI 场景对应起来</h3><p>重点掌握：</p><ul><li>向量和矩阵的基本运算</li><li>内积、矩阵乘法的几何和计算意义</li><li>范数、距离、相似度</li><li>张量的概念</li><li>线性变换在神经网络里的作用</li></ul><p>学这一块时，不要只停留在纸面。<br>一定要拿 <code>NumPy</code> 或 <code>PyTorch</code> 自己写一写矩阵乘法、线性层、向量相似度。</p><h3 id="第三步：把概率统计从“背公式”变成“会解释”">第三步：把概率统计从“背公式”变成“会解释”</h3><p>重点掌握：</p><ul><li>概率分布是什么</li><li>条件概率和贝叶斯公式是什么意思</li><li>似然函数和极大似然估计在做什么</li><li>期望、方差如何描述随机变量</li><li>交叉熵为什么适合做分类和语言模型训练</li></ul><p>这一块一旦打通，你对“模型输出概率分布”这件事就会有本质理解。</p><h3 id="第四步：把数学和代码打通">第四步：把数学和代码打通</h3><p>很多人学数学卡住，不是因为概念太难，而是因为概念和代码之间断了层。</p><p>所以一定要做这类练习：</p><ul><li>用 <code>NumPy</code> 实现线性回归</li><li>手写梯度下降</li><li>手写 <code>softmax</code></li><li>手写交叉熵</li><li>理解一个两层神经网络前向和反向传播在做什么</li></ul><p>当你能把公式翻译成代码时，理解会发生质变。</p><h3 id="第五步：再去看深度学习和大模型">第五步：再去看深度学习和大模型</h3><p>这时候你再去看这些内容，会轻松很多：</p><ul><li>神经网络基础</li><li>反向传播</li><li>优化器</li><li>Embedding</li><li>Attention</li><li>Transformer</li><li>预训练、微调、对齐</li></ul><p>你会发现，大量“看起来很高级的概念”，不过是基础数学在更复杂系统中的组合应用。</p><hr><h2 id="八、最后做一个总结：你真正要建立的，不是公式记忆，而是数学直觉">八、最后做一个总结：你真正要建立的，不是公式记忆，而是数学直觉</h2><p>到这里，我们回头看整件事。</p><p>为什么 LLM 时代还必须补数学基础？</p><p>因为大模型并没有消灭数学，只是把数学藏到了更深的地方。</p><p>你在界面上看到的是对话、生成、推理、检索、Agent、工作流。<br>但在模型内部，真正发生的是：</p><ul><li>文本被编码成向量</li><li>向量经过矩阵变换</li><li>模型输出概率分布</li><li>损失函数衡量预测质量</li><li>梯度把误差信号传回参数</li><li>参数在无数次更新中逐渐学到规律</li></ul><p>这就是现代 AI 的底层逻辑。</p><p>所以，如果你是初学者，我最想给你的建议不是：</p><p>“赶紧去把所有公式背下来。”</p><p>而是：</p><p><strong>先理解每个数学概念到底在描述什么现实问题。</strong></p><p>当你这样学的时候，你会发现：</p><ul><li>导数，不再只是求切线斜率，而是在回答“参数该怎么调”</li><li>向量，不再只是几列数字，而是在回答“信息该怎么表示”</li><li>概率，不再只是考试题，而是在回答“模型如何面对不确定性”</li></ul><p>当这些点连起来以后，机器学习、深度学习、LLM 对你来说，就不再是一堆神秘黑箱，而会慢慢变成一张清晰的知识地图。</p><p>如果你愿意继续往下走，那么下一阶段你可以开始系统学三件事：</p><ol><li>用代码实现几个最基础的机器学习模型</li><li>用 <code>PyTorch</code> 理解前向传播和反向传播</li><li>带着“向量 + 概率 + 优化”的视角再去看 Transformer</li></ol><p>当你再回头看今天的大模型时，你会更容易意识到一件事：</p><p><strong>所谓 AI 的“智能感”，很多时候并不是凭空出现的，而是数学、数据、计算和工程共同作用的结果。</strong></p><p>这也正是它迷人的地方。</p><hr><h2 id="附：如果你只想先记住-10-句话">附：如果你只想先记住 10 句话</h2><ol><li>导数描述的是变化率，模型靠它知道参数该往哪边调。</li><li>偏导数处理的是多变量问题，梯度是所有偏导数组成的方向向量。</li><li>梯度下降的本质，是沿着损失下降最快的方向更新参数。</li><li>链式法则是反向传播的数学基础。</li><li>向量和矩阵是 AI 表示数据、组织计算的基本语言。</li><li>矩阵乘法本质上是在做线性变换和特征重组。</li><li>概率分布描述的是“可能性”，不是绝对确定性。</li><li>似然函数研究的是“哪组参数最能解释数据”。</li><li>交叉熵会惩罚模型低估真实答案的行为。</li><li>LLM 的核心训练目标，可以理解为预测下一个 token 的条件概率。</li></ol><p>如果你能把这 10 句话真正吃透，那么你已经走在理解 AI 原理的正确路上了。</p>]]></content>
    
    
    <summary type="html">LLM时代为什么一定要补数学基础：从导数、向量到深度学习与大模型</summary>
    
    
    
    <category term="LLM" scheme="https://yjyrichard.github.io/categories/LLM/"/>
    
    
    <category term="LLM" scheme="https://yjyrichard.github.io/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>从零讲明白：语言学与自然语言处理</title>
    <link href="https://yjyrichard.github.io/posts/63064e94.html"/>
    <id>https://yjyrichard.github.io/posts/63064e94.html</id>
    <published>2026-04-09T15:06:11.316Z</published>
    <updated>2026-04-09T15:19:02.209Z</updated>
    
    <content type="html"><![CDATA[<h1>从零讲明白：语言学与自然语言处理</h1><blockquote><p>一篇写给小白的入门文章。<br>如果你曾经觉得“自然语言处理”这六个字看起来很厉害、很学术、很像会出现在考试最后一道大题里，那么恭喜你，这篇就是专门来把它讲人话的。</p></blockquote><hr><h2 id="壹、先别怕：这门学问到底在研究什么？">壹、先别怕：这门学问到底在研究什么？</h2><p>我们先不急着谈“模型”“算法”“大语言模型”这些听起来很像要熬夜的词。</p><p>先看一个最朴素的问题：</p><p><strong>人是怎么沟通的？机器又是怎么沟通的？为什么人和机器一交流就容易卡壳？</strong></p><p>大体上，沟通可以分成三种情况：</p><h3 id="1-人和人沟通">1. 人和人沟通</h3><p>比如：</p><ul><li>“你吃饭了吗？”</li><li>“今天天气不错。”</li><li>“这道题我不会，救命。”</li></ul><p>这是最自然的一种沟通方式。我们用汉语、英语、日语这些语言交流，这些语言都属于<strong>自然语言</strong>。</p><h3 id="2-机器和机器沟通">2. 机器和机器沟通</h3><p>比如程序之间传数据、服务器返回 JSON、代码执行指令。</p><p>机器之间也能“交流”，但它们用的不是人话，而是：</p><ul><li>程序语言</li><li>指令</li><li>协议</li><li>数据格式</li></ul><p>这一类语言有个共同特点：<strong>规则明确、结构严格、歧义极少。</strong></p><h3 id="3-人和机器沟通">3. 人和机器沟通</h3><p>问题来了。</p><p>人会说：</p><ul><li>“帮我查一下明天北京天气。”</li><li>“给我推荐一部轻松一点的电影。”</li><li>“我饿了，附近有啥好吃的？”</li></ul><p>但机器最早并不擅长理解这种自然、口语化、上下文很多的话。</p><p>于是就出现一种很经典的局面：</p><blockquote><p><strong>无法沟通！</strong></p></blockquote><p>而<strong>自然语言处理（NLP）</strong>，本质上就是来解决第三种情况的。</p><p>说得再直白一点：</p><blockquote><p><strong>自然语言处理，就是研究怎样让机器听懂人话、看懂人话、甚至说出人话。</strong></p></blockquote><hr><h2 id="貳、什么是“语言学”？">貳、什么是“语言学”？</h2><p>很多人一看到“语言学”三个字，会下意识想到：</p><ul><li>语文课</li><li>语法书</li><li>病句修改</li><li>修辞手法</li><li>背单词</li></ul><p>这些当然和语言有关，但<strong>语言学</strong>并不只是这些。</p><h3 id="1-语言学的定义">1. 语言学的定义</h3><p>语言学，是<strong>研究人类语言本身的科学</strong>。</p><p>它关心的问题包括：</p><ul><li>语言的声音是怎么产生的？</li><li>词是怎么构成的？</li><li>句子为什么能这样组织？</li><li>一句话的意义是怎么产生的？</li><li>为什么同一句话，换个场景意思就不一样了？</li></ul><p>简单说，语言学要回答的是：</p><blockquote><p><strong>语言到底是怎样运作的？</strong></p></blockquote><h3 id="2-语言学不是背规则，而是研究规律">2. 语言学不是背规则，而是研究规律</h3><p>比如我们说：</p><ul><li>“我喜欢你。”</li><li>“你喜欢我。”</li></ul><p>字都一样，只是顺序变了，意思就完全不同。</p><p>再比如：</p><ul><li>“苹果很好。”</li></ul><p>这里的“苹果”到底是水果，还是苹果公司，还是 iPhone？</p><p>人脑通常很快就能判断，但机器不一定行。</p><p>语言学就是研究这种“看起来很自然，其实背后全是规律和坑”的东西。</p><hr><h2 id="參、什么是“自然语言”？">參、什么是“自然语言”？</h2><h3 id="1-自然语言的定义">1. 自然语言的定义</h3><p><strong>自然语言</strong>，就是人类在长期社会生活中自然形成并使用的语言。</p><p>比如：</p><ul><li>汉语</li><li>英语</li><li>日语</li><li>法语</li><li>阿拉伯语</li></ul><p>这些都属于自然语言。</p><p>这里的“自然”，不是“天然无添加”的意思，而是说：</p><blockquote><p>它不是专门为了机器设计出来的，而是人类自己慢慢发展出来的。</p></blockquote><h3 id="2-自然语言和程序语言有什么不同？">2. 自然语言和程序语言有什么不同？</h3><p>这个区别特别重要。</p><table><thead><tr><th>对比项</th><th>自然语言</th><th>程序语言/形式语言</th></tr></thead><tbody><tr><td>使用者</td><td>人类</td><td>机器、人类程序员</td></tr><tr><td>形成方式</td><td>长期自然演化</td><td>人工设计</td></tr><tr><td>规则</td><td>有规则，但很灵活</td><td>规则严格</td></tr><tr><td>歧义</td><td>很多</td><td>尽量没有</td></tr><tr><td>是否依赖语境</td><td>非常依赖</td><td>尽量不依赖</td></tr></tbody></table><p>比如这句：</p><p><strong>“苹果很好。”</strong></p><p>在自然语言里，它可能有多种意思；<br>但如果放在程序语言里，变量、函数、语法都要定义清楚，否则程序直接报错，不跟你讲感情。</p><h3 id="3-为什么自然语言难处理？">3. 为什么自然语言难处理？</h3><p>因为它不是为机器准备的。</p><p>人类说话时会：</p><ul><li>省略</li><li>暗示</li><li>反讽</li><li>模糊表达</li><li>依赖背景知识</li><li>一词多义</li><li>一句多解</li></ul><p>人听人话没问题，是因为人脑自动补了很多“隐藏信息”。</p><p>机器：</p><blockquote><p>“你们人类讲话能不能稍微像写代码一样规范一点？”</p></blockquote><p>显然不能。所以 NLP 才难。</p><hr><h2 id="肆、为什么机器听不懂人话？">肆、为什么机器听不懂人话？</h2><p>这个问题，是整门课的核心。</p><p>很多人会想：</p><blockquote><p>“一句话而已，人一眼就懂，机器怎么会不懂？”</p></blockquote><p>原因在于，人类所谓的“懂”，并不是只看字面，而是综合了：</p><ul><li>语境</li><li>常识</li><li>情景</li><li>经验</li><li>说话人的意图</li><li>文化背景</li></ul><p>而机器一开始并没有这些能力。</p><p>下面看几个最典型的难点。</p><h3 id="1-语言有歧义">1. 语言有歧义</h3><h4 id="词义歧义">词义歧义</h4><p>比如：</p><p><strong>“苹果很好。”</strong></p><p>这里的“苹果”可能是：</p><ul><li>水果</li><li>苹果公司</li><li>苹果手机</li></ul><h4 id="句法歧义">句法歧义</h4><p>比如：</p><p><strong>“我看见拿望远镜的人。”</strong></p><p>到底是：</p><ul><li>我拿着望远镜，看见了那个人；<br>还是</li><li>我看见了那个拿望远镜的人。</li></ul><h4 id="指代歧义">指代歧义</h4><p>比如：</p><p><strong>“小王告诉小李，他通过了考试。”</strong></p><p>“他”是谁？</p><ul><li>小王？</li><li>小李？</li></ul><p>机器会非常头大。</p><h3 id="2-语言依赖上下文">2. 语言依赖上下文</h3><p>比如一句：</p><p><strong>“可以。”</strong></p><p>这句话单拿出来几乎什么都说明不了。</p><p>它可能是：</p><ul><li>同意</li><li>敷衍</li><li>允许</li><li>勉强接受</li><li>不想继续争论</li></ul><p>再比如：</p><p><strong>“你真行。”</strong></p><p>有时候是夸奖，<br>有时候其实是：“你可真会给我整活。”</p><h3 id="3-语言依赖常识">3. 语言依赖常识</h3><p>比如：</p><p><strong>“杯子倒了，水洒了。”</strong></p><p>人当然知道：</p><ul><li>杯子一般能装水</li><li>杯子倒了，水可能流出来</li></ul><p>但机器并不是天生就知道这些。</p><h3 id="4-语言经常不规范">4. 语言经常不规范</h3><p>现实中的人类说话，和课本上的句子完全不是一个画风。</p><p>比如：</p><ul><li>“在吗”</li><li>“笑死”</li><li>“我真的会谢”</li><li>“这也太离谱了吧”</li><li>“懂的都懂”</li></ul><p>这些表达里，有大量：</p><ul><li>省略</li><li>口语化</li><li>情绪色彩</li><li>隐含信息</li></ul><p>人懂，机器未必懂。</p><h3 id="5-中文还有自己的额外难题">5. 中文还有自己的额外难题</h3><p>英文大多数时候天然有空格：</p><blockquote><p>I love natural language processing.</p></blockquote><p>机器比较容易知道哪个是哪个词。</p><p>但中文通常写成：</p><blockquote><p>我喜欢自然语言处理</p></blockquote><p>机器首先要回答一个非常基础的问题：</p><blockquote><p><strong>到底该怎么切词？</strong></p></blockquote><p>比如它可能切成：</p><ul><li>我 / 喜欢 / 自然语言处理</li></ul><p>也可能切成：</p><ul><li>我 / 喜欢 / 自然 / 语言 / 处理</li></ul><p>你看，中文连“词在哪儿”都得先判断，后面当然更难。</p><hr><h2 id="伍、语言学通常研究语言的哪些层次？">伍、语言学通常研究语言的哪些层次？</h2><p>语言不是一个平面，它像洋葱一样，一层一层剥下去，每一层都有学问。</p><p>通常可以把语言大致分成下面几个层次。</p><h3 id="1-语音层：声音是怎么回事">1. 语音层：声音是怎么回事</h3><p>这部分研究：</p><ul><li>发音</li><li>音节</li><li>音位</li><li>声调</li><li>语音差别</li></ul><p>比如中文里：</p><ul><li>妈</li><li>麻</li><li>马</li><li>骂</li></ul><p>字形很像，发音的声调不同，意思就完全不同。</p><p>如果一个系统要做语音识别，它就得尽量区分这种差别。</p><p>所以，语音层更接近“机器怎么听懂你说的话”。</p><hr><h3 id="2-词法层：词是怎么长出来的">2. 词法层：词是怎么长出来的</h3><p>这部分研究：</p><ul><li>词怎么构成</li><li>词和词之间的内部关系</li><li>词形变化</li><li>构词方式</li></ul><p>比如：</p><ul><li>学</li><li>学习</li><li>学习者</li><li>可学习性</li></ul><p>这些词之间并不是完全独立的，它们有构成关系。</p><p>在英文里会更明显：</p><ul><li>teach</li><li>teacher</li><li>teaching</li><li>taught</li></ul><p>NLP 在这一层常见的事情有：</p><ul><li>分词</li><li>词形还原</li><li>词干提取</li><li>词法分析</li></ul><hr><h3 id="3-句法层：词怎么拼成句子">3. 句法层：词怎么拼成句子</h3><p>这部分研究的是：</p><blockquote><p><strong>词和词之间，究竟是按照什么关系组织成句子的。</strong></p></blockquote><p>比如：</p><ul><li>“猫追老鼠。”</li><li>“老鼠追猫。”</li></ul><p>字都认识，词也没变，顺序一换，意义立刻反过来。</p><p>再比如：</p><p><strong>“小明喜欢打篮球。”</strong></p><p>你会自然理解：</p><ul><li>“小明”是主语</li><li>“喜欢”是动作或状态</li><li>“打篮球”是喜欢的内容</li></ul><p>NLP 在句法层会做：</p><ul><li>词性标注</li><li>句法分析</li><li>成分分析</li><li>依存分析</li></ul><p>简单理解就是：</p><blockquote><p>让机器知道，谁和谁关系最近，谁修饰谁，谁是动作发出者，谁是承受者。</p></blockquote><hr><h3 id="4-语义层：这句话到底是什么意思">4. 语义层：这句话到底是什么意思</h3><p>语义研究的是意义。</p><p>比如：</p><p><strong>“苹果很好吃。”</strong></p><p>这里的“苹果”大概率是水果。</p><p>但：</p><p><strong>“苹果发布了新手机。”</strong></p><p>这里的“苹果”就不可能是水果，而是公司。</p><p>同一个词，放在不同句子里，意义会变。</p><p>再比如：</p><ul><li>“老师批评了学生。”</li><li>“学生被老师批评了。”</li></ul><p>表达形式不一样，但核心事件其实差不多。</p><p>语义层想抓住的，就是这种更深层的“意思”。</p><hr><h3 id="5-语用层：人说的话，不一定只等于字面">5. 语用层：人说的话，不一定只等于字面</h3><p>语用层研究的是：</p><blockquote><p><strong>一句话在真实场景中，到底是在干什么。</strong></p></blockquote><p>比如冬天你走进屋里说：</p><p><strong>“有点冷啊。”</strong></p><p>字面上是在描述温度，<br>但很多时候真正的意思可能是：</p><ul><li>把窗关一下</li><li>开空调</li><li>谁来救救我的手</li></ul><p>再比如：</p><p><strong>“你能把门关上吗？”</strong></p><p>字面上像是在问能力，<br>实际上大多数时候是在提出请求。</p><p>所以语言理解到最后，经常不只是“识字”和“懂语法”，而是要明白：</p><blockquote><p><strong>说话人真正想表达什么。</strong></p></blockquote><hr><h2 id="陸、什么是自然语言处理（NLP）？">陸、什么是自然语言处理（NLP）？</h2><p>讲到这里，自然语言处理就好理解了。</p><h3 id="1-NLP-的定义">1. NLP 的定义</h3><p><strong>自然语言处理（Natural Language Processing，简称 NLP）</strong>，就是研究如何让计算机对自然语言进行：</p><ul><li>处理</li><li>理解</li><li>分析</li><li>生成</li><li>应用</li></ul><p>的一门交叉学科。</p><p>它站在几个学科的交叉口上：</p><ul><li><strong>语言学</strong>：研究语言本身</li><li><strong>计算机科学</strong>：让机器能处理这些语言</li><li><strong>数学/统计学</strong>：让机器从数据中学习规律</li><li><strong>人工智能</strong>：让系统表现出更强的理解和生成能力</li></ul><h3 id="2-NLP-的目标，用一句大白话来说">2. NLP 的目标，用一句大白话来说</h3><blockquote><p><strong>让机器尽可能像一个“能听、能读、能理解、能表达”的交流对象。</strong></p></blockquote><p>当然，现实里“尽可能”这三个字很重要。因为真正让机器像人一样理解语言，依然是一件非常难的事。</p><h3 id="3-NLP-不是只有聊天机器人">3. NLP 不是只有聊天机器人</h3><p>很多人一提 NLP，就想到聊天、问答、AI 助手。</p><p>这些当然算，但它远不止这些。</p><p>NLP 的范围很广，从最基础的分词，到高级的对话生成，都算。</p><hr><h2 id="柒、自然语言处理具体在做哪些事？">柒、自然语言处理具体在做哪些事？</h2><p>下面这些，就是 NLP 里非常经典的任务。</p><h3 id="1-分词">1. 分词</h3><p>这是中文里特别重要的一步。</p><p>比如：</p><p><strong>“我喜欢自然语言处理”</strong></p><p>分词后可能变成：</p><ul><li>我 / 喜欢 / 自然语言处理</li></ul><p>如果连词都切不准，后面很多分析都会跟着歪。</p><hr><h3 id="2-词性标注">2. 词性标注</h3><p>就是判断每个词是什么词类。</p><p>比如：</p><p><strong>“小明喜欢篮球。”</strong></p><p>可能标注成：</p><ul><li>小明（名词）</li><li>喜欢（动词）</li><li>篮球（名词）</li></ul><p>这能帮助机器理解句子结构。</p><hr><h3 id="3-命名实体识别">3. 命名实体识别</h3><p>识别文本中的特定名称，比如：</p><ul><li>人名</li><li>地名</li><li>机构名</li><li>时间</li><li>金额</li></ul><p>比如：</p><p><strong>“马云在杭州创办了阿里巴巴。”</strong></p><p>系统要尽量识别出：</p><ul><li>马云 → 人名</li><li>杭州 → 地名</li><li>阿里巴巴 → 机构名</li></ul><hr><h3 id="4-句法分析">4. 句法分析</h3><p>这一步想解决的是：</p><ul><li>谁干了什么</li><li>谁修饰谁</li><li>词和词之间是什么关系</li></ul><p>比如：</p><p><strong>“小张昨天在图书馆借了一本书。”</strong></p><p>系统希望能分析出：</p><ul><li>谁借的 → 小张</li><li>什么时候 → 昨天</li><li>在哪里 → 图书馆</li><li>借了什么 → 一本书</li></ul><hr><h3 id="5-语义分析">5. 语义分析</h3><p>语义分析关注的是：</p><blockquote><p><strong>这句话究竟表达了什么含义。</strong></p></blockquote><p>比如：</p><p><strong>“我想订一张明天去上海的高铁票。”</strong></p><p>系统不仅要看懂字面，还要理解：</p><ul><li>意图：订票</li><li>时间：明天</li><li>目的地：上海</li><li>对象：高铁票</li></ul><hr><h3 id="6-情感分析">6. 情感分析</h3><p>判断一句话表达的是正面、负面还是中性。</p><p>比如：</p><ul><li>“这家店服务很好。” → 正面</li><li>“等了两小时还没上菜。” → 负面</li><li>“这家店在商场三楼。” → 中性</li></ul><p>这个任务常用于：</p><ul><li>评论分析</li><li>舆情监测</li><li>用户反馈整理</li></ul><hr><h3 id="7-文本分类">7. 文本分类</h3><p>把一段文本归入某个类别。</p><p>比如：</p><ul><li>新闻分类：体育 / 财经 / 娱乐 / 科技</li><li>邮件分类：垃圾邮件 / 正常邮件</li><li>客服问题分类：物流 / 支付 / 售后</li></ul><hr><h3 id="8-信息抽取">8. 信息抽取</h3><p>从文本里提取结构化信息。</p><p>比如：</p><p><strong>“张三于 2025 年 5 月加入某公司担任算法工程师。”</strong></p><p>可以抽取成：</p><ul><li>人物：张三</li><li>时间：2025 年 5 月</li><li>机构：某公司</li><li>职位：算法工程师</li></ul><hr><h3 id="9-机器翻译">9. 机器翻译</h3><p>把一种自然语言转换成另一种自然语言。</p><p>比如：</p><ul><li>中文 → 英文</li><li>英文 → 日文</li></ul><p>翻译不是简单地一词换一词，还要考虑：</p><ul><li>语序</li><li>搭配</li><li>习惯表达</li><li>语境</li><li>文化差异</li></ul><hr><h3 id="10-自动摘要">10. 自动摘要</h3><p>给长文章写短总结。</p><p>比如一篇几千字的新闻，系统生成三五句话的核心概述。</p><p>这在信息太多、时间太少的世界里非常实用。</p><hr><h3 id="11-问答系统与对话系统">11. 问答系统与对话系统</h3><p>比如你问：</p><ul><li>“北京明天会下雨吗？”</li><li>“什么是卷积神经网络？”</li><li>“帮我写一封请假邮件。”</li></ul><p>系统需要：</p><ul><li>理解问题</li><li>找到信息</li><li>组织答案</li><li>用自然语言回复你</li></ul><p>这类能力已经广泛出现在：</p><ul><li>智能客服</li><li>搜索引擎</li><li>AI 助手</li><li>教育系统</li></ul><hr><h3 id="12-文本生成">12. 文本生成</h3><p>让机器自己写出语言。</p><p>比如：</p><ul><li>写摘要</li><li>写邮件</li><li>写广告语</li><li>续写故事</li><li>回答问题</li></ul><p>这也是如今大语言模型最引人关注的一部分。</p><hr><h2 id="捌、机器到底是怎么“理解”语言的？">捌、机器到底是怎么“理解”语言的？</h2><p>这里一定要讲清楚一个关键事实：</p><blockquote><p><strong>机器并不是像人一样直接“看到文字就懂意思”。</strong></p></blockquote><p>它首先看到的，只是一串符号。</p><h3 id="1-对机器来说，文字一开始只是符号">1. 对机器来说，文字一开始只是符号</h3><p>人看到“猫”，脑海里会自动联想到：</p><ul><li>一种动物</li><li>有毛</li><li>会叫</li><li>可能会掉毛</li><li>有时候高冷得像房东</li></ul><p>但机器不会。</p><p>机器一开始并不知道“猫”是什么。它只知道：</p><ul><li>这是一个字符</li><li>这是某种编码</li><li>它和别的符号不同</li></ul><p>所以 NLP 的一个核心工作就是：</p><blockquote><p><strong>把语言变成机器可以处理的形式。</strong></p></blockquote><h3 id="2-语言最终要表示成数字">2. 语言最终要表示成数字</h3><p>计算机最擅长处理的是数字，不是“意义”本身。</p><p>因此 NLP 里有一个很关键的问题：</p><blockquote><p><strong>怎样把词、句子、文章表示成数字？</strong></p></blockquote><p>这一步叫做<strong>文本表示</strong>。</p><p>最早比较朴素的方法，是：</p><ul><li>给每个词一个编号</li></ul><p>但这样机器只知道它们不同，不知道谁和谁更像。</p><p>后来更聪明的方法出现了：</p><ul><li>用向量表示词</li><li>用上下文表示句子</li><li>让模型从大量数据里学习词与词、句与句之间的关系</li></ul><p>于是机器开始能学到一些“相似性”和“上下文差异”。</p><h3 id="3-语料库很重要">3. 语料库很重要</h3><p>如果把机器学习语言这件事比作“上课”，<br>那么<strong>语料库</strong>就像教材、题库和练习册。</p><p>语料库就是大量真实语言材料的集合，比如：</p><ul><li>新闻</li><li>小说</li><li>论文</li><li>对话</li><li>评论</li><li>网页文本</li></ul><p>机器通过大量语料，慢慢学到：</p><ul><li>哪些词常一起出现</li><li>哪种表达更常见</li><li>某个词在什么语境里大概率是什么意思</li></ul><h3 id="4-NLP-的发展大致经历了几个阶段">4. NLP 的发展大致经历了几个阶段</h3><h4 id="（1）规则方法">（1）规则方法</h4><p>早期人们想：既然语言有规则，那就把规则写出来。</p><p>优点：</p><ul><li>逻辑清楚</li><li>可解释</li></ul><p>缺点：</p><ul><li>规则写不完</li><li>很难覆盖真实语言中的各种变化</li></ul><h4 id="（2）统计方法">（2）统计方法</h4><p>后来大家发现，可以从大量文本中统计规律。</p><p>核心想法是：</p><blockquote><p>语言中有大量可以通过数据观察出来的模式。</p></blockquote><h4 id="（3）深度学习方法">（3）深度学习方法</h4><p>再后来，模型开始能自动学习更复杂的表示，不再完全依赖人工写特征。</p><p>这一阶段推动了很多能力飞速提升。</p><h4 id="（4）大语言模型阶段">（4）大语言模型阶段</h4><p>如今的大模型能：</p><ul><li>对话</li><li>总结</li><li>翻译</li><li>推理</li><li>生成内容</li></ul><p>但即便如此，它们也不等于“像人一样完全理解语言”。</p><p>这点要清醒。</p><hr><h2 id="玖、语言学和自然语言处理，到底是什么关系？">玖、语言学和自然语言处理，到底是什么关系？</h2><p>这是很多初学者最容易混淆的地方。</p><h3 id="1-语言学研究“语言是什么”">1. 语言学研究“语言是什么”</h3><p>它更偏向理论层面，研究：</p><ul><li>语言的结构</li><li>语言的规律</li><li>语言的层次</li><li>语言为什么会歧义</li><li>人是怎么理解话语的</li></ul><h3 id="2-自然语言处理研究“机器怎么处理语言”">2. 自然语言处理研究“机器怎么处理语言”</h3><p>它更偏向工程与应用层面，研究：</p><ul><li>怎么设计程序</li><li>怎么表示语言</li><li>怎么训练模型</li><li>怎么完成分词、翻译、问答等任务</li></ul><h3 id="3-二者不是对立关系，而是互相支撑">3. 二者不是对立关系，而是互相支撑</h3><p>可以把它们理解成：</p><ul><li><strong>语言学像地图</strong>：告诉你语言世界的结构长什么样</li><li><strong>NLP 像施工队</strong>：负责把“让机器处理语言”这件事真的做出来</li></ul><p>没有语言学，NLP 容易变成“只会调包，不知道自己在解决什么问题”；<br>没有计算机和算法，语言学的很多想法也很难真正落地到系统里。</p><p>所以它们的关系非常紧密。</p><hr><h2 id="拾、举一个完整例子：机器如何处理一句人话？">拾、举一个完整例子：机器如何处理一句人话？</h2><p>我们来看一句日常表达：</p><blockquote><p><strong>“帮我订一张明天去上海的高铁票。”</strong></p></blockquote><p>人类一看就懂，但机器得分很多步来做。</p><h3 id="第一步：分词">第一步：分词</h3><p>可能先切成：</p><ul><li>帮我 / 订 / 一张 / 明天 / 去 / 上海 / 的 / 高铁票</li></ul><h3 id="第二步：识别关键信息">第二步：识别关键信息</h3><p>系统可能识别出：</p><ul><li>动作：订</li><li>时间：明天</li><li>地点：上海</li><li>对象：高铁票</li></ul><h3 id="第三步：识别用户意图">第三步：识别用户意图</h3><p>机器要判断出：</p><blockquote><p>这不是闲聊，而是在发起一个“订票请求”。</p></blockquote><h3 id="第四步：判断信息是否完整">第四步：判断信息是否完整</h3><p>它可能继续发现：</p><ul><li>从哪里出发？不知道</li><li>几点出发？不知道</li><li>几等座？不知道</li></ul><p>于是它会继续追问：</p><ul><li>“请问您从哪里出发？”</li><li>“您想买上午还是下午的车次？”</li></ul><h3 id="第五步：生成回应">第五步：生成回应</h3><p>最后再返回结果、确认订单或继续对话。</p><p>你会发现，这里面并不是只有一个技术，而是很多能力一起工作：</p><ul><li>分词</li><li>实体识别</li><li>句法和语义分析</li><li>意图识别</li><li>对话管理</li><li>文本生成</li></ul><p>这就是 NLP 在真实应用中的样子。</p><hr><h2 id="拾壹、初学者最容易踩的几个误区">拾壹、初学者最容易踩的几个误区</h2><h3 id="误区-1：NLP-就是聊天机器人">误区 1：NLP 就是聊天机器人</h3><p>不是。</p><p>聊天只是 NLP 的一个应用方向。</p><p>NLP 还包括：</p><ul><li>搜索</li><li>分类</li><li>翻译</li><li>摘要</li><li>信息抽取</li><li>情感分析</li><li>语音转文本后的理解</li></ul><h3 id="误区-2：语言处理就是字符串处理">误区 2：语言处理就是字符串处理</h3><p>也不是。</p><p>字符串处理只是在处理字符表面形式，<br>而 NLP 更关心：</p><ul><li>结构</li><li>关系</li><li>语义</li><li>意图</li><li>上下文</li></ul><p>比如“苹果”这两个字是字符串问题；<br>“苹果在这里指水果还是公司”是语言理解问题。</p><h3 id="误区-3：机器能回答，不等于它真的像人一样理解">误区 3：机器能回答，不等于它真的像人一样理解</h3><p>这一点特别重要。</p><p>机器有时会表现得“像懂了”，<br>但它未必是以人的方式理解。</p><p>它可能更多是在：</p><ul><li>学模式</li><li>学统计规律</li><li>学上下文关联</li><li>学如何生成合理回复</li></ul><p>所以学习 NLP 时，既要看到它很强，也要知道它并不是魔法。</p><h3 id="误区-4：只要会调模型，就等于懂-NLP">误区 4：只要会调模型，就等于懂 NLP</h3><p>不完全对。</p><p>如果你只会调用现成模型，却不理解：</p><ul><li>为什么会歧义</li><li>什么是句法问题</li><li>什么是语义问题</li><li>为什么中文分词困难</li></ul><p>那你在遇到真实问题时，往往很难定位错误。</p><p>懂一点语言学，会让你更知道自己到底在处理什么。</p><hr><h2 id="拾貳、如果你是零基础，应该怎样理解这门课？">拾貳、如果你是零基础，应该怎样理解这门课？</h2><p>你可以先死死记住三句话：</p><h3 id="第一句">第一句</h3><blockquote><p><strong>语言学研究“语言是什么”。</strong></p></blockquote><h3 id="第二句">第二句</h3><blockquote><p><strong>自然语言处理研究“机器怎么处理语言”。</strong></p></blockquote><h3 id="第三句">第三句</h3><blockquote><p><strong>NLP 的核心目标，是解决人与机器之间“用自然语言沟通”这件事。</strong></p></blockquote><p>如果你把这三句话真正吃透，那么这门课的门，已经算是被你推开了。</p><hr><h2 id="拾參、送你一份小白版学习路线图（文字版思维导图）">拾參、送你一份小白版学习路线图（文字版思维导图）</h2><p>既然这部分资料本身就和“语言学与自然语言处理”入门有关，那这里顺手给你一份<strong>可以直接照着走的路线图</strong>。</p><h3 id="第一层：先把概念搞清楚">第一层：先把概念搞清楚</h3><p>先理解这些词到底是什么意思：</p><ul><li>语言学</li><li>自然语言</li><li>自然语言处理</li><li>语料库</li><li>分词</li><li>句法</li><li>语义</li><li>语用</li></ul><p>目标：</p><blockquote><p>不再把 NLP 当成一个玄学名词，而是真正知道它在研究什么。</p></blockquote><h3 id="第二层：理解语言的几个层次">第二层：理解语言的几个层次</h3><p>按顺序掌握：</p><ol><li>语音</li><li>词法</li><li>句法</li><li>语义</li><li>语用</li></ol><p>目标：</p><blockquote><p>知道语言问题为什么能拆层，知道“机器听不懂人话”到底难在哪一层。</p></blockquote><h3 id="第三层：认识-NLP-的经典任务">第三层：认识 NLP 的经典任务</h3><p>重点认识：</p><ul><li>分词</li><li>词性标注</li><li>命名实体识别</li><li>句法分析</li><li>文本分类</li><li>情感分析</li><li>信息抽取</li><li>机器翻译</li><li>摘要</li><li>问答与对话</li></ul><p>目标：</p><blockquote><p>明白 NLP 不是一个点，而是一整套任务群。</p></blockquote><h3 id="第四层：补一点技术基础">第四层：补一点技术基础</h3><p>如果后面你要继续深入，可以逐步补：</p><ul><li>Python 基础</li><li>数据结构与基础编程</li><li>概率统计基础</li><li>机器学习基础</li><li>深度学习基础</li></ul><p>目标：</p><blockquote><p>开始具备“把语言问题交给机器解决”的技术工具。</p></blockquote><h3 id="第五层：进入现代-NLP">第五层：进入现代 NLP</h3><p>之后再继续往下走：</p><ul><li>词向量</li><li>序列模型</li><li>Transformer</li><li>预训练模型</li><li>大语言模型</li></ul><p>目标：</p><blockquote><p>从“知道 NLP 是什么”，走向“知道现代 NLP 为什么这么强”。</p></blockquote><h3 id="第六层：一定要做项目">第六层：一定要做项目</h3><p>不做项目，很多理解都只是浮在表面。</p><p>可以尝试：</p><ul><li>做一个情感分析小项目</li><li>做一个文本分类器</li><li>做一个命名实体识别 Demo</li><li>调用模型做一个简单问答系统</li></ul><p>目标：</p><blockquote><p>把抽象概念变成真正能运行的东西。</p></blockquote><hr><h2 id="拾肆、把全文压成一张“考前小抄”">拾肆、把全文压成一张“考前小抄”</h2><p>如果你现在只想记最关键的内容，那就记下面这些：</p><ol><li><strong>语言学</strong>是研究人类语言规律的学科。</li><li><strong>自然语言</strong>是人类自然形成的语言，比如中文、英文。</li><li>**自然语言处理（NLP）**是让机器处理、理解和生成自然语言的技术。</li><li>NLP 的核心目标，是解决“人和机器怎么自然沟通”的问题。</li><li>语言难，是因为它有歧义、依赖上下文、依赖常识，而且不总是规范。</li><li>语言可以从语音、词法、句法、语义、语用等层次来研究。</li><li>NLP 的经典任务包括分词、词性标注、实体识别、句法分析、情感分析、翻译、摘要、问答等。</li><li>机器不是直接“懂文字”，而是先把语言变成可以计算的表示。</li><li>语料库是机器学习语言的重要材料来源。</li><li>语言学和 NLP 是互相支撑的：一个研究语言，一个让机器处理语言。</li></ol><hr><h2 id="拾伍、把抽象概念塞回生活：你的一天里，其实全是-NLP">拾伍、把抽象概念塞回生活：你的一天里，其实全是 NLP</h2><p>很多小白会有一种错觉：</p><blockquote><p>“自然语言处理听起来很高端，应该离我的生活很远吧？”</p></blockquote><p>其实恰恰相反。</p><p>你不但接触过 NLP，而且你<strong>几乎每天都在用</strong>。</p><p>我们来过一遍非常普通、甚至有点平凡的一天。</p><h3 id="1-早上，你对手机说：“今天天气怎么样？”">1. 早上，你对手机说：“今天天气怎么样？”</h3><p>这背后至少发生了几件事：</p><ul><li>语音识别：先把你说的话变成文字</li><li>语言理解：判断你是在查询天气</li><li>信息抽取：识别“今天”“天气”这类关键信息</li><li>文本生成：组织回答内容</li><li>语音合成：再把回答念给你听</li></ul><p>你嘴里轻轻一句“今天天气怎么样”，<br>机器背后已经忙成流水线了。</p><h3 id="2-你打字时，输入法在猜你想说什么">2. 你打字时，输入法在猜你想说什么</h3><p>你刚输入：</p><ul><li>“自然语”</li></ul><p>输入法就开始给你推荐：</p><ul><li>自然语言</li><li>自然语言处理</li><li>自然语义</li></ul><p>它为什么能猜？</p><p>因为它会参考：</p><ul><li>常见词搭配</li><li>你以前的输入习惯</li><li>当前上下文</li><li>哪些词在真实文本中更常一起出现</li></ul><p>这其实就是 NLP 的语言建模思想在发挥作用。</p><h3 id="3-你去搜：“附近便宜又好吃的面馆”">3. 你去搜：“附近便宜又好吃的面馆”</h3><p>表面上你只是搜了一句话，<br>实际上搜索系统要判断很多事：</p><ul><li>你想找的是“餐馆”，不是食谱</li><li>“附近”说明跟位置有关</li><li>“便宜”是价格偏好</li><li>“好吃”带有主观评价需求</li><li>“面馆”限制了商家类型</li></ul><p>也就是说，它不是只在匹配字，而是在尽量理解你的意图。</p><h3 id="4-你刷视频，看到自动字幕">4. 你刷视频，看到自动字幕</h3><p>自动字幕的背后，首先是语音识别；<br>字幕出来之后，如果系统还会：</p><ul><li>自动纠错</li><li>自动总结</li><li>自动翻译</li><li>自动提取关键词</li></ul><p>那就已经明显进入 NLP 的工作范围了。</p><h3 id="5-你看电商评论，平台给你总结“大家都在夸什么”">5. 你看电商评论，平台给你总结“大家都在夸什么”</h3><p>比如系统告诉你：</p><ul><li>优点：物流快、包装好、味道不错</li><li>缺点：价格偏高、分量偏少</li></ul><p>这背后可能用到了：</p><ul><li>情感分析</li><li>观点抽取</li><li>文本聚类</li><li>自动摘要</li></ul><p>也就是说，机器在替你把一大堆人话，压缩成你能快速看懂的信息。</p><h3 id="6-你找客服，先遇到智能机器人">6. 你找客服，先遇到智能机器人</h3><p>你问：</p><ul><li>“我的快递怎么还没到？”</li></ul><p>系统要尽量判断：</p><ul><li>这是在问物流，不是退款</li><li>你的情绪可能有点着急</li><li>它应该优先返回物流状态或转人工</li></ul><p>如果它回答得还算像样，那说明背后已经做了不少语言理解工作。</p><h3 id="7-你读外文文章，顺手点一下翻译">7. 你读外文文章，顺手点一下翻译</h3><p>你看到一篇英文资料，点一下翻译，几秒后就有中文结果。</p><p>这背后不是“字典逐字替换”，而是：</p><ul><li>理解原句结构</li><li>处理词义差异</li><li>调整语序</li><li>选择更自然的目标语言表达</li></ul><p>这就是机器翻译。</p><h3 id="8-晚上你问-AI：“帮我写一段请假理由”">8. 晚上你问 AI：“帮我写一段请假理由”</h3><p>这是更典型的 NLP 场景。</p><p>系统需要：</p><ul><li>理解你的需求</li><li>判断文体</li><li>组织句子</li><li>输出一段像人写的话</li></ul><p>所以你可以把这整件事记成一句话：</p><blockquote><p><strong>你不是没接触过 NLP，而是你早就生活在 NLP 里了。</strong></p></blockquote><hr><h2 id="拾陸、中文-NLP-为什么尤其让机器头疼？">拾陸、中文 NLP 为什么尤其让机器头疼？</h2><p>如果说自然语言处理本来就难，<br>那么中文自然语言处理，经常还要在这个“难”字前面再加一个“更”。</p><p>为什么？因为中文有一些非常有特色、也非常能折磨机器的地方。</p><h3 id="1-中文没有天然空格，词边界不明显">1. 中文没有天然空格，词边界不明显</h3><p>英文写成：</p><blockquote><p>I love natural language processing.</p></blockquote><p>机器一眼大概就知道词在哪里。</p><p>但中文常常写成：</p><blockquote><p>我喜欢自然语言处理</p></blockquote><p>机器得先猜：</p><ul><li>我 / 喜欢 / 自然语言处理</li></ul><p>还是：</p><ul><li>我 / 喜欢 / 自然 / 语言 / 处理</li></ul><p>再比如经典例子：</p><ul><li>南京市长江大桥</li><li>研究生命起源</li></ul><p>到底怎么切，直接影响后面所有理解。</p><h3 id="2-中文特别爱省略">2. 中文特别爱省略</h3><p>日常对话里，人类很爱把话说一半。</p><p>比如：</p><ul><li>“吃了吗？”</li><li>“到了。”</li><li>“看过了。”</li><li>“可以，发我。”</li></ul><p>这些句子里，主语、宾语、背景信息，经常都被省略了。</p><p>人类靠语境自动补全：</p><ul><li>谁到了？</li><li>到哪儿了？</li><li>什么看过了？</li><li>发什么给谁？</li></ul><p>机器如果没有上下文，就会一脸茫然。</p><h3 id="3-中文词类有时很灵活">3. 中文词类有时很灵活</h3><p>同一个词，在不同句子里，角色可能不一样。</p><p>比如“研究”：</p><ul><li>“他在研究这个问题。” → 这里更像动词</li><li>“这个研究很有价值。” → 这里更像名词</li></ul><p>再比如“影响”：</p><ul><li>“这会影响结果。” → 动词</li><li>“这件事的影响很大。” → 名词</li></ul><p>这就意味着，机器不能只看词表，还得看上下文。</p><h3 id="4-中文特别依赖语境和语气">4. 中文特别依赖语境和语气</h3><p>比如：</p><p><strong>“你真行。”</strong></p><p>它可能是：</p><ul><li>真心夸奖</li><li>阴阳怪气</li><li>无奈吐槽</li></ul><p>如果你只看字面，很容易误判。</p><p>再比如：</p><p><strong>“行啊。”</strong></p><p>有时候是爽快答应，<br>有时候是“我记住你了”的前奏。</p><p>这类微妙差异，对机器非常不友好。</p><h3 id="5-中文里有大量成语、俗语、网络语">5. 中文里有大量成语、俗语、网络语</h3><p>比如：</p><ul><li>一石二鸟</li><li>对牛弹琴</li><li>破防了</li><li>栓Q</li><li>YYDS</li><li>我真的会谢</li></ul><p>这些表达如果按字面理解，常常会翻车。</p><p>比如“我真的会谢”，<br>大多数时候不是在表达“我要去感谢别人”，<br>而是在表达一种无语、崩溃、哭笑不得的情绪。</p><h3 id="6-中文文本常常缺少语音信息">6. 中文文本常常缺少语音信息</h3><p>面对面说话时，人能靠：</p><ul><li>语调</li><li>表情</li><li>停顿</li><li>重音</li></ul><p>来判断意思。</p><p>但到了纯文本里，这些信息往往都不见了。</p><p>于是像下面这种话就很难：</p><ul><li>“可以啊。”</li><li>“你厉害。”</li><li>“不错不错。”</li></ul><p>到底是夸，还是反讽？<br>很多时候，人都得猜，更别说机器。</p><h3 id="7-中文里还有很多“人类懂，但不好讲清”的习惯">7. 中文里还有很多“人类懂，但不好讲清”的习惯</h3><p>比如：</p><ul><li>“把字句”</li><li>“被字句”</li><li>话题先行</li><li>省主语</li><li>连续短句</li></ul><p>这些东西，中国人平时说起来完全自然，<br>但真让你写成规则给机器看，你会发现：</p><blockquote><p>原来我们会说话，不代表我们会把“为什么这样说”解释清楚。</p></blockquote><p>这也是为什么语言学很重要。</p><p>它不是来让你背得更痛苦的，<br>它是来帮你把“本来觉得理所当然的语言现象”拆开讲明白的。</p><p>一句话总结这一节：</p><blockquote><p><strong>中文 NLP 难，不只是因为中文复杂，而是因为中文在“省略、语境、歧义、灵活表达”这些方面都很强。</strong></p></blockquote><hr><h2 id="拾柒、如果老师突然问你：什么是语言学与自然语言处理？你可以怎么答">拾柒、如果老师突然问你：什么是语言学与自然语言处理？你可以怎么答</h2><p>这一节很适合两种人：</p><ul><li>一种是要考试的人</li><li>另一种是明明懂了一点，但一张嘴就卡壳的人</li></ul><p>下面给你两个模板。</p><h3 id="1-一句话版本">1. 一句话版本</h3><blockquote><p><strong>语言学研究人类语言的结构、规律和使用方式；自然语言处理研究怎样让计算机处理、理解和生成人类自然语言。</strong></p></blockquote><p>如果只能说一句，这句已经够用了。</p><h3 id="2-三十秒版本">2. 三十秒版本</h3><blockquote><p>语言学是研究人类语言本身的学科，关注语音、词法、句法、语义、语用等层次；自然语言处理是在语言学、计算机科学和人工智能交叉基础上，研究如何让机器理解和使用自然语言的一门技术。它的目标是解决人和机器之间的语言沟通问题。</p></blockquote><h3 id="3-一分钟版本">3. 一分钟版本</h3><blockquote><p>语言学主要研究语言是什么、语言有哪些规律，比如词怎么构成、句子怎么组织、意义怎么产生。自然语言处理则更关心机器怎么处理这些语言，比如分词、词性标注、情感分析、机器翻译、问答系统等。因为自然语言存在歧义、依赖上下文和常识，所以让机器真正理解语言是一件很难但很重要的事。</p></blockquote><h3 id="4-考试答题展开模板">4. 考试答题展开模板</h3><p>如果题目比较大，你可以按这个顺序展开：</p><h4 id="第一步：先下定义">第一步：先下定义</h4><ul><li>语言学是什么</li><li>自然语言是什么</li><li>自然语言处理是什么</li></ul><h4 id="第二步：再讲为什么难">第二步：再讲为什么难</h4><ul><li>有歧义</li><li>依赖语境</li><li>依赖常识</li><li>中文还涉及分词、省略、口语表达等问题</li></ul><h4 id="第三步：再讲语言有哪些层次">第三步：再讲语言有哪些层次</h4><ul><li>语音</li><li>词法</li><li>句法</li><li>语义</li><li>语用</li></ul><h4 id="第四步：再讲-NLP-能做什么">第四步：再讲 NLP 能做什么</h4><ul><li>分词</li><li>词性标注</li><li>命名实体识别</li><li>句法分析</li><li>情感分析</li><li>翻译</li><li>摘要</li><li>问答系统</li></ul><h4 id="第五步：最后讲关系">第五步：最后讲关系</h4><ul><li>语言学提供理论基础</li><li>NLP 负责技术实现与应用落地</li></ul><p>这样答出来，结构会非常清楚。</p><p>一句话送给容易紧张的同学：</p><blockquote><p><strong>答大题时，最怕的不是不会，而是脑子里有东西但没有顺序。</strong></p></blockquote><p>而这一节，就是帮你把顺序理出来。</p><hr><h2 id="拾捌、小白真正可执行的学习路线：从“看懂概念”到“做出一个小-Demo”">拾捌、小白真正可执行的学习路线：从“看懂概念”到“做出一个小 Demo”</h2><p>很多人学 NLP，会在两个坑里反复横跳：</p><ul><li>要么一直停留在背概念</li><li>要么一上来就冲大模型，结果连问题本身都没看明白</li></ul><p>更稳妥的方式，是一层一层来。</p><h3 id="第壹层：先把“语言问题”看明白">第壹层：先把“语言问题”看明白</h3><p>你先别急着训练模型。</p><p>先把这些概念真正搞懂：</p><ul><li>自然语言是什么</li><li>为什么自然语言难</li><li>语音、词法、句法、语义、语用分别是什么</li><li>分词、实体识别、情感分析这些任务到底在做什么</li></ul><p>这一层的目标不是会写代码，<br>而是你看到一句话时，能开始有意识地想：</p><ul><li>这是词义歧义吗？</li><li>这是句法问题吗？</li><li>这是语义问题还是语用问题？</li></ul><p>当你会这样想时，你就已经不是纯小白了。</p><h3 id="第貳层：补上最基本的工具能力">第貳层：补上最基本的工具能力</h3><p>如果你想继续往下走，就要具备最基本的技术工具：</p><ul><li>会一点 Python</li><li>会读写文本文件</li><li>知道什么是数据集</li><li>知道怎么把文本整理成机器能处理的形式</li></ul><p>你不一定一开始就要会特别多数学，<br>但至少要知道：</p><ul><li>数据从哪里来</li><li>数据怎么清洗</li><li>标签是什么</li><li>输入和输出分别是什么</li></ul><h3 id="第參层：先做经典小任务，别一上来就挑战宇宙难题">第參层：先做经典小任务，别一上来就挑战宇宙难题</h3><p>最适合入门的任务通常是：</p><ul><li>文本分类</li><li>情感分析</li><li>命名实体识别</li><li>关键词提取</li><li>简单问答或检索</li></ul><p>为什么这些适合入门？</p><p>因为它们：</p><ul><li>目标相对清楚</li><li>输入输出比较明确</li><li>容易验证效果</li><li>做出来之后成就感也比较足</li></ul><p>比如，你完全可以从下面这些小项目开始：</p><h4 id="小项目-1：做一个评论情感判断器">小项目 1：做一个评论情感判断器</h4><p>输入一句评论：</p><ul><li>“这家店很好吃，下次还来。”</li></ul><p>输出：</p><ul><li>正面</li></ul><h4 id="小项目-2：做一个新闻分类器">小项目 2：做一个新闻分类器</h4><p>输入一段新闻，输出：</p><ul><li>体育 / 财经 / 科技 / 娱乐</li></ul><h4 id="小项目-3：做一个实体识别-Demo">小项目 3：做一个实体识别 Demo</h4><p>输入：</p><ul><li>“马云在杭州创办了阿里巴巴。”</li></ul><p>输出：</p><ul><li>人名：马云</li><li>地名：杭州</li><li>机构名：阿里巴巴</li></ul><p>这些项目都不一定惊天动地，<br>但它们会让你第一次真正感受到：</p><blockquote><p>原来“处理语言”这件事，是可以一步一步做出来的。</p></blockquote><h3 id="第肆层：再去理解现代-NLP-模型为什么强">第肆层：再去理解现代 NLP 模型为什么强</h3><p>等你对问题本身有感觉了，再去看现代模型，会顺很多。</p><p>这时你可以逐步接触：</p><ul><li>词向量</li><li>序列模型</li><li>注意力机制</li><li>Transformer</li><li>预训练模型</li><li>大语言模型</li></ul><p>这时候你会发现，你不是在背术语，而是在回答一个问题：</p><blockquote><p><strong>这些模型，到底是怎么让机器更会“处理语言”的？</strong></p></blockquote><h3 id="第伍层：一定要做“完整流程”项目">第伍层：一定要做“完整流程”项目</h3><p>真正让人进步最快的，不是只看一段代码，<br>而是亲手走完一遍完整流程：</p><ol><li>选问题</li><li>收集或整理数据</li><li>明确输入输出</li><li>训练或调用模型</li><li>看结果</li><li>分析错误</li><li>继续改进</li></ol><p>注意，“分析错误”特别重要。</p><p>因为很多时候，真正的成长不是来自“模型跑通了”，而是来自：</p><ul><li>你发现它为什么错</li><li>你知道这个错是词义问题、句法问题，还是数据问题</li><li>你知道下一步该改哪里</li></ul><h3 id="第陸层：学会从“任务视角”而不是“模型视角”看问题">第陸层：学会从“任务视角”而不是“模型视角”看问题</h3><p>初学者很容易掉进一个坑：</p><ul><li>天天记模型名字</li><li>Transformer、BERT、GPT、RAG、一堆缩写全背了</li></ul><p>但一遇到真实任务，就不知道从哪儿下手。</p><p>更好的思路是反过来：</p><ul><li>我的问题是什么？</li><li>输入是什么？</li><li>输出是什么？</li><li>难点是什么？</li><li>评价标准是什么？</li></ul><p>当你先看任务，再选方法，思路会清楚很多。</p><p>一句话总结这一整节：</p><blockquote><p><strong>学 NLP，先理解语言问题，再学技术工具；先做小任务，再看大模型；先能分析问题，再追求模型高级。</strong></p></blockquote><hr><h2 id="拾玖、术语速查表：把这篇里最容易混的词一次讲清">拾玖、术语速查表：把这篇里最容易混的词一次讲清</h2><p>下面这张表，你可以把它当成“看完文章后的快速翻译器”。</p><table><thead><tr><th>术语</th><th>大白话解释</th></tr></thead><tbody><tr><td>语言学</td><td>研究人类语言规律的学科</td></tr><tr><td>自然语言</td><td>人类自然形成并使用的语言，如中文、英文</td></tr><tr><td>NLP</td><td>让机器处理、理解、生成自然语言的技术</td></tr><tr><td>语料库</td><td>大量真实语言材料的集合，是机器学习语言的重要数据来源</td></tr><tr><td>分词</td><td>把连续文本切成词，中文里尤其重要</td></tr><tr><td>词性标注</td><td>判断一个词在句子里是名词、动词还是别的词类</td></tr><tr><td>句法</td><td>研究词和词如何组成句子</td></tr><tr><td>语义</td><td>研究一句话或一个词表达的意义</td></tr><tr><td>语用</td><td>研究一句话在具体场景里真正想表达什么</td></tr><tr><td>命名实体识别</td><td>识别人名、地名、机构名、时间等专有信息</td></tr><tr><td>文本分类</td><td>给一段文字打标签，比如体育、财经、科技</td></tr><tr><td>情感分析</td><td>判断文本表达的是正面、负面还是中性情绪</td></tr><tr><td>信息抽取</td><td>从自然语言中提取结构化信息</td></tr><tr><td>机器翻译</td><td>把一种语言自动转成另一种语言</td></tr><tr><td>自动摘要</td><td>把长文本压缩成短总结</td></tr><tr><td>文本表示</td><td>把语言转成机器可计算的数字形式</td></tr><tr><td>上下文</td><td>一句话所在的前后环境，常常决定真正含义</td></tr><tr><td>歧义</td><td>同一句话或同一个词可能有多个解释</td></tr><tr><td>模型</td><td>机器学习到的一套处理模式或计算结构</td></tr><tr><td>大语言模型</td><td>在海量文本上训练出来、能生成和理解语言的强大模型</td></tr></tbody></table><p>如果你现在看到这些词，不再只觉得“好像听过”，而是能大概讲出意思，说明你已经前进很大一步了。</p><hr><h2 id="貳拾、来做几个小测试：看看你到底懂了没有">貳拾、来做几个小测试：看看你到底懂了没有</h2><p>下面这些问题，不是为了吓你，<br>而是为了帮你判断：你到底是“看过了”，还是“真的懂了”。</p><h3 id="1-什么是自然语言？">1. 什么是自然语言？</h3><p>参考答案：</p><blockquote><p>自然语言是人类在长期社会生活中自然形成并使用的语言，比如中文、英文。它不是专门为机器设计的，所以常常有歧义、依赖语境，也更难处理。</p></blockquote><h3 id="2-为什么自然语言处理很难？">2. 为什么自然语言处理很难？</h3><p>参考答案：</p><blockquote><p>因为自然语言不是严格、唯一、无歧义的。它有一词多义、一句多解、依赖上下文、依赖常识、表达不规范等特点，所以机器很难像人一样自然理解它。</p></blockquote><h3 id="3-语言学和自然语言处理有什么区别？">3. 语言学和自然语言处理有什么区别？</h3><p>参考答案：</p><blockquote><p>语言学研究语言本身的结构和规律，偏理论；自然语言处理研究机器怎样处理语言，偏技术与应用。前者提供理解语言的视角，后者负责把处理语言的能力落到系统里。</p></blockquote><h3 id="4-“苹果发布了新手机”里的“苹果”和“苹果很好吃”里的“苹果”，为什么是个典型-NLP-问题？">4. “苹果发布了新手机”里的“苹果”和“苹果很好吃”里的“苹果”，为什么是个典型 NLP 问题？</h3><p>参考答案：</p><blockquote><p>因为这里涉及词义歧义。同一个词在不同上下文里含义不同，机器需要根据句子环境判断它到底指水果还是公司。</p></blockquote><h3 id="5-语义和语用有什么区别？">5. 语义和语用有什么区别？</h3><p>参考答案：</p><blockquote><p>语义关注“字面或结构上的意思是什么”；语用关注“在具体场景里，说这句话真正想表达什么”。比如“有点冷啊”在语义上是在描述温度，在语用上可能是在请求别人关窗。</p></blockquote><h3 id="6-为什么中文-NLP-往往比很多人想象中更难？">6. 为什么中文 NLP 往往比很多人想象中更难？</h3><p>参考答案：</p><blockquote><p>因为中文没有天然空格，先要解决分词问题；同时中文还大量依赖语境，喜欢省略，词类灵活，口语和网络表达丰富，这些都会增加机器理解难度。</p></blockquote><h3 id="7-NLP-常见任务有哪些？">7. NLP 常见任务有哪些？</h3><p>参考答案：</p><blockquote><p>分词、词性标注、命名实体识别、句法分析、语义分析、情感分析、文本分类、信息抽取、机器翻译、自动摘要、问答与对话系统等。</p></blockquote><h3 id="8-如果让你用一句话概括-NLP-的目标，你会怎么说？">8. 如果让你用一句话概括 NLP 的目标，你会怎么说？</h3><p>参考答案：</p><blockquote><p>NLP 的目标是让机器尽可能理解、处理和生成人类自然语言，从而改善人和机器之间的沟通。</p></blockquote><p>如果上面这 8 个问题你已经能顺下来讲得七七八八，<br>那说明你对这篇内容已经不是“看热闹”，而是真入门了。</p><hr><h2 id="貳拾壹、最后用一句话收尾">貳拾壹、最后用一句话收尾</h2><p>如果要把“语言学与自然语言处理”这门内容浓缩成一句最通俗的话，那就是：</p><blockquote><p><strong>语言学在研究：人类是怎么用语言表达世界的；自然语言处理在研究：怎样让机器也尽量学会这件事。</strong></p></blockquote><p>而这件事之所以迷人，就在于：</p><ul><li>它既有人文学科对语言的观察，</li><li>又有计算机科学对问题的解决，</li><li>还夹着一点人工智能对“理解”这件事的野心。</li></ul><p>说得俏皮一点就是：</p><blockquote><p><strong>语言学负责研究“人为什么这么说”；NLP 负责研究“机器怎么才能别再听不懂”。</strong></p></blockquote><p>如果你读到这里，已经不再觉得这几个词只是“听起来很厉害的黑话”，<br>而是开始能把它们讲给别人听、拿来答题、甚至拿来规划自己的学习路线，<br>那这篇文章就真的完成任务了。</p>]]></content>
    
    
    <summary type="html">语言学与自然语言处理</summary>
    
    
    
    <category term="NLP" scheme="https://yjyrichard.github.io/categories/NLP/"/>
    
    
    <category term="NLP" scheme="https://yjyrichard.github.io/tags/NLP/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 40 课：综合项目与进阶路线建议</title>
    <link href="https://yjyrichard.github.io/posts/34f24360.html"/>
    <id>https://yjyrichard.github.io/posts/34f24360.html</id>
    <published>2026-03-22T15:28:11.780Z</published>
    <updated>2026-03-22T15:40:40.047Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 40 课：综合项目与进阶路线建议</h1><blockquote><p>学习定位：这是整套 Go 教程的第 40 课，也是阶段六（高级进阶阶段）的最后一课，同时也是整套课程的收束课。<br>前置要求：已经完成前 39 课，掌握 Go 的基础语法、标准库、并发、测试、性能分析、工程组织以及高级理解主题。<br>本课目标：把整套课程中的知识点串成一张完整能力地图，理解一个综合 Go 项目应该如何设计、拆分、测试和迭代，明确后续可选进阶方向，并形成一套可执行的长期学习路线。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>如果说前 39 课是在“逐块搭积木”，那这一课要做的事就是：</p><p><strong>把这些积木真正拼成一座房子。</strong></p><p>学到这里，你可能已经掌握了很多分散的知识点：</p><ul><li>会写 Go 基础语法</li><li>会用结构体、方法、接口</li><li>会处理文件、时间、JSON、字符串</li><li>会写 goroutine、channel、context</li><li>会写测试、Benchmark、<code>pprof</code></li><li>知道项目怎么分层</li><li>也开始理解一些底层行为</li></ul><p>但真正决定你能不能进入实战的，不是“点状会不会”，而是：</p><ul><li>你能不能把这些知识串起来</li><li>你能不能设计一个完整的小项目</li><li>你能不能知道下一步该往哪个方向深入</li></ul><p>这节课就要解决三个问题：</p><ol><li>你现在到底具备了哪些 Go 能力</li><li>一个综合 Go 项目应该怎么落地</li><li>课程结束后，你该怎么继续进阶</li></ol><hr><h2 id="2-学完这-40-课，你已经具备了什么能力">2. 学完这 40 课，你已经具备了什么能力</h2><p>先不要急着看后面的路线，先把你已经学到的东西看清楚。</p><h3 id="2-1-语言基础能力">2.1 语言基础能力</h3><p>你已经掌握了：</p><ul><li>变量、常量、类型系统</li><li>条件、循环、函数</li><li>指针、数组、切片、map</li><li>字符串、结构体、方法、接口</li><li>错误处理、<code>defer</code>、<code>panic</code>、<code>recover</code></li><li>包与模块管理</li></ul><p>这意味着：</p><p>你已经不只是“会写 Hello World”，而是具备了独立阅读和编写一般 Go 代码的能力。</p><h3 id="2-2-标准库实战能力">2.2 标准库实战能力</h3><p>你已经接触过：</p><ul><li>文件操作</li><li>时间处理</li><li>字符串处理</li><li>JSON 编解码</li><li>排序与常用工具</li></ul><p>这意味着：</p><p>你已经能做出很多真实的小工具和命令行程序。</p><h3 id="2-3-并发与工程能力">2.3 并发与工程能力</h3><p>你已经学过：</p><ul><li>goroutine</li><li>channel</li><li>select</li><li>同步原语</li><li>context</li><li>测试</li><li>Benchmark</li><li>工程组织</li></ul><p>这意味着：</p><p>你已经开始进入“能写项目”的阶段，而不只是“会写几个小函数”。</p><h3 id="2-4-高级理解能力">2.4 高级理解能力</h3><p>你已经接触过：</p><ul><li>泛型</li><li>反射</li><li>内存与逃逸分析</li><li>切片、map、接口底层</li><li><code>pprof</code></li><li>源码阅读方法</li></ul><p>这意味着：</p><p>你已经具备了继续深入 Go 的认知基础。</p><hr><h2 id="3-现在最重要的事：从“学知识点”转向“做完整项目”">3. 现在最重要的事：从“学知识点”转向“做完整项目”</h2><p>很多人课程学到这里，下一步容易犯两个错误。</p><h3 id="3-1-错误一：继续无止境刷零散知识点">3.1 错误一：继续无止境刷零散知识点</h3><p>比如：</p><ul><li>再看 20 篇语法小文章</li><li>再记 50 个小技巧</li><li>再收藏一堆“面试八股”</li></ul><p>这样当然也有价值，但增长会开始变慢。</p><h3 id="3-2-错误二：直接跳进很大的框架或复杂项目">3.2 错误二：直接跳进很大的框架或复杂项目</h3><p>比如一上来就去做：</p><ul><li>大型微服务</li><li>Kubernetes Operator</li><li>分布式中间件</li></ul><p>这通常跨度过大，很容易挫败。</p><h3 id="3-3-正确路线：做一个完整但可控的综合项目">3.3 正确路线：做一个完整但可控的综合项目</h3><p>你现在最需要的，不是更多零散知识，而是一次完整串联。</p><p>也就是做一个项目，把这些能力都连起来：</p><ul><li>配置</li><li>路由 / 输入输出</li><li>业务逻辑</li><li>存储</li><li>错误处理</li><li>日志</li><li>测试</li><li>性能分析</li></ul><p>这一步做完，你的能力会明显从“学习态”进入“工程态”。</p><hr><h2 id="4-一个综合-Go-项目应该长什么样">4. 一个综合 Go 项目应该长什么样</h2><p>对于当前阶段，我建议你的综合项目满足这几个条件：</p><ol><li>有真实输入输出</li><li>有明确业务对象</li><li>有数据存储或状态管理</li><li>有错误处理和日志</li><li>有测试</li><li>有一点性能或工程化思考</li></ol><h3 id="4-1-适合当前阶段的项目类型">4.1 适合当前阶段的项目类型</h3><p>比如：</p><ul><li>任务管理系统</li><li>简单博客后端</li><li>文件同步工具</li><li>批量日志分析器</li><li>小型爬虫与数据处理工具</li><li>简单 HTTP API 服务</li></ul><h3 id="4-2-不建议作为第一综合项目的类型">4.2 不建议作为第一综合项目的类型</h3><p>比如：</p><ul><li>分布式消息队列</li><li>全功能电商系统</li><li>复杂微服务网关</li><li>自己手搓数据库</li></ul><p>不是不能做，而是作为第一综合项目，难度和收益比通常不合适。</p><h3 id="4-3-一个实用标准">4.3 一个实用标准</h3><p>好的一阶段收官项目，应该让你：</p><ul><li>需要用到前面学过的大部分知识</li><li>但仍然能在 1～3 周内完成一个可运行版本</li></ul><hr><h2 id="5-推荐综合项目示例：任务管理-HTTP-服务">5. 推荐综合项目示例：任务管理 HTTP 服务</h2><p>为了把前面的知识串起来，这里给你一个非常适合当前阶段的示例项目：</p><blockquote><p>一个支持新增、查询、完成、删除任务的 HTTP 服务。</p></blockquote><h3 id="5-1-为什么推荐它">5.1 为什么推荐它</h3><p>因为它刚好能覆盖：</p><ul><li>HTTP 接口</li><li>JSON 编解码</li><li>项目结构</li><li>业务层与存储层拆分</li><li>配置</li><li>日志</li><li>错误处理</li><li>测试</li><li>后续还可以扩展并发和性能分析</li></ul><h3 id="5-2-最小功能集">5.2 最小功能集</h3><ul><li><code>POST /tasks</code>：创建任务</li><li><code>GET /tasks</code>：获取任务列表</li><li><code>PATCH /tasks/&#123;id&#125;/done</code>：标记完成</li><li><code>DELETE /tasks/&#123;id&#125;</code>：删除任务</li></ul><h3 id="5-3-数据结构示意">5.3 数据结构示意</h3><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="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID        <span class="type">int</span>       <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Title     <span class="type">string</span>    <span class="string">`json:&quot;title&quot;`</span></span><br><span class="line">    Done      <span class="type">bool</span>      <span class="string">`json:&quot;done&quot;`</span></span><br><span class="line">    CreatedAt time.Time <span class="string">`json:&quot;created_at&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个项目不复杂，但足够完整。</p><hr><h2 id="6-这个综合项目该怎么拆分">6. 这个综合项目该怎么拆分</h2><p>结合你前面学过的工程组织，一个比较健康的结构可以是：</p><figure class="highlight text"><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">task-service/</span><br><span class="line">├── go.mod</span><br><span class="line">├── cmd/</span><br><span class="line">│   └── task-service/</span><br><span class="line">│       └── main.go</span><br><span class="line">├── internal/</span><br><span class="line">│   ├── config/</span><br><span class="line">│   ├── logger/</span><br><span class="line">│   ├── task/</span><br><span class="line">│   ├── store/</span><br><span class="line">│   └── httpapi/</span><br><span class="line">├── testdata/</span><br><span class="line">└── README.md</span><br></pre></td></tr></table></figure><h3 id="6-1-各目录职责">6.1 各目录职责</h3><ul><li><code>cmd/task-service/main.go</code>：程序入口，组装依赖</li><li><code>internal/config</code>：读取配置</li><li><code>internal/logger</code>：初始化日志</li><li><code>internal/task</code>：任务领域模型与业务逻辑</li><li><code>internal/store</code>：存储实现，先从内存或文件版开始</li><li><code>internal/httpapi</code>：HTTP handler、请求解析、响应输出</li></ul><h3 id="6-2-为什么这样拆">6.2 为什么这样拆</h3><p>因为这正好体现了你前面学过的几个工程原则：</p><ul><li>入口尽量薄</li><li>业务逻辑不要堆在 <code>main</code></li><li>存储细节不要反向污染业务层</li><li>配置、日志统一管理</li></ul><p>这已经是一个很像样的小型工程骨架了。</p><hr><h2 id="7-如何把前面-39-课的知识串进这个项目">7. 如何把前面 39 课的知识串进这个项目</h2><p>这一节是整课的核心之一。</p><h3 id="7-1-基础语法阶段如何落地">7.1 基础语法阶段如何落地</h3><p>你前面学的：</p><ul><li>结构体</li><li>方法</li><li>接口</li><li>错误处理</li></ul><p>会直接落在：</p><ul><li><code>Task</code> 结构体建模</li><li><code>Service</code> 方法设计</li><li><code>Repository</code> 接口抽象</li><li>业务错误定义</li></ul><h3 id="7-2-标准库阶段如何落地">7.2 标准库阶段如何落地</h3><p>你前面学的：</p><ul><li><code>encoding/json</code></li><li><code>time</code></li><li>文件操作</li><li>字符串处理</li></ul><p>会直接落在：</p><ul><li>请求 / 响应 JSON</li><li>创建时间字段</li><li>文件持久化版本</li><li>输入校验和文本处理</li></ul><h3 id="7-3-并发阶段如何落地">7.3 并发阶段如何落地</h3><p>你前面学的：</p><ul><li>goroutine</li><li>channel</li><li>context</li><li>同步原语</li></ul><p>可以逐步扩展到：</p><ul><li>后台异步写日志</li><li>请求超时控制</li><li>并发安全的内存存储</li><li>后台任务处理</li></ul><h3 id="7-4-工程与高级阶段如何落地">7.4 工程与高级阶段如何落地</h3><p>你前面学的：</p><ul><li>测试</li><li>Benchmark</li><li><code>pprof</code></li><li>逃逸分析</li><li>源码阅读方法</li></ul><p>可以逐步扩展到：</p><ul><li>为业务层写单元测试</li><li>为关键函数做基准测试</li><li>对热点接口做 Profile</li><li>对字符串处理和切片分配做优化</li><li>参考标准库重构 API 风格</li></ul><p>这就是课程真正完成闭环的地方。</p><hr><h2 id="8-综合项目应该按什么顺序做">8. 综合项目应该按什么顺序做</h2><p>不要一上来同时搞完所有东西。</p><p>一个比较稳的顺序是：</p><h3 id="8-1-第一步：做最小可运行版本">8.1 第一步：做最小可运行版本</h3><p>先完成：</p><ul><li>项目结构</li><li>内存存储</li><li>4 个 HTTP 接口</li><li>最基础的错误返回</li></ul><p>这一步目标只有一个：</p><blockquote><p>跑起来。</p></blockquote><h3 id="8-2-第二步：补工程化骨架">8.2 第二步：补工程化骨架</h3><p>再加：</p><ul><li>配置加载</li><li>日志</li><li>存储层抽象</li><li>文件存储版本</li></ul><p>这一步目标是：</p><blockquote><p>让它不像 demo，而像项目。</p></blockquote><h3 id="8-3-第三步：补测试">8.3 第三步：补测试</h3><p>优先补：</p><ul><li>业务层单元测试</li><li>表驱动测试</li><li>存储层边界测试</li></ul><p>这一步目标是：</p><blockquote><p>让后续改动变安全。</p></blockquote><h3 id="8-4-第四步：做性能和可维护性改进">8.4 第四步：做性能和可维护性改进</h3><p>例如：</p><ul><li>关键路径 Benchmark</li><li>JSON 处理优化</li><li>减少不必要分配</li><li>API 命名和错误语义调整</li></ul><p>这一步目标是：</p><blockquote><p>从“能用”提升到“更像成熟工程”。</p></blockquote><hr><h2 id="9-一个完整-Go-项目，至少要考虑哪几层">9. 一个完整 Go 项目，至少要考虑哪几层</h2><p>这部分以后你做项目时可以直接当检查清单。</p><h3 id="9-1-输入层">9.1 输入层</h3><p>比如：</p><ul><li>CLI 参数</li><li>HTTP 请求</li><li>配置文件</li><li>环境变量</li></ul><p>要考虑：</p><ul><li>输入解析</li><li>参数校验</li><li>错误提示</li></ul><h3 id="9-2-业务层">9.2 业务层</h3><p>这里应该放：</p><ul><li>核心规则</li><li>状态变更</li><li>业务判断</li></ul><p>要避免：</p><ul><li>直接耦合 HTTP</li><li>直接耦合数据库细节</li></ul><h3 id="9-3-存储层">9.3 存储层</h3><p>这里负责：</p><ul><li>文件</li><li>内存</li><li>数据库</li></ul><p>要考虑：</p><ul><li>接口抽象</li><li>错误上下文</li><li>是否需要并发安全</li></ul><h3 id="9-4-工程辅助层">9.4 工程辅助层</h3><p>比如：</p><ul><li>配置</li><li>日志</li><li>测试</li><li>指标</li><li>Profile</li></ul><p>这一层虽然不直接体现业务，但决定项目是否“能长期维护”。</p><hr><h2 id="10-学完课程后，你的下一个重点不应该只是“更多知识”，而是“更多作品”">10. 学完课程后，你的下一个重点不应该只是“更多知识”，而是“更多作品”</h2><p>这是非常关键的一条建议。</p><h3 id="10-1-为什么作品比继续刷零散教程更重要">10.1 为什么作品比继续刷零散教程更重要</h3><p>因为作品会逼着你面对真实问题：</p><ul><li>项目结构怎么拆</li><li>错误怎么设计</li><li>日志怎么打</li><li>测试怎么补</li><li>性能问题怎么定位</li></ul><p>这些能力，单靠看文章很难真正长出来。</p><h3 id="10-2-什么叫“有效作品”">10.2 什么叫“有效作品”</h3><p>不是指：</p><ul><li>代码量特别大</li><li>功能特别花哨</li></ul><p>而是指：</p><ul><li>结构清晰</li><li>功能完整</li><li>有文档</li><li>有测试</li><li>能运行</li><li>你能讲清楚为什么这么设计</li></ul><p>这类作品对成长最有价值。</p><hr><h2 id="11-后续进阶方向怎么选">11. 后续进阶方向怎么选</h2><p>学完这套课程后，你大概会分成几个主要方向。</p><h3 id="11-1-方向一：Web-后端">11.1 方向一：Web 后端</h3><p>适合你如果：</p><ul><li>想做业务系统</li><li>想做 API 服务</li><li>想做后台开发</li></ul><p>建议下一步重点学：</p><ul><li><code>net/http</code></li><li>路由和中间件</li><li>数据库访问</li><li>用户认证</li><li>配置与部署</li></ul><p>典型项目：</p><ul><li>博客后端</li><li>用户系统</li><li>订单系统</li><li>管理后台 API</li></ul><h3 id="11-2-方向二：微服务与后端架构">11.2 方向二：微服务与后端架构</h3><p>适合你如果：</p><ul><li>已经有一些 Web 基础</li><li>想进一步做服务拆分和架构设计</li></ul><p>建议下一步重点学：</p><ul><li>RPC</li><li>服务治理</li><li>配置中心</li><li>服务发现</li><li>链路追踪</li><li>消息队列</li></ul><p>这条路线更适合在完成 1～2 个完整服务项目后继续深入。</p><h3 id="11-3-方向三：云原生与基础设施">11.3 方向三：云原生与基础设施</h3><p>适合你如果：</p><ul><li>对系统层、自动化、平台工程更感兴趣</li></ul><p>建议下一步重点学：</p><ul><li>Docker</li><li>Kubernetes</li><li>Operator</li><li>控制器模式</li><li>可观测性</li><li>Go 在云原生生态中的常见写法</li></ul><p>这条路线会大量用到你已经学过的：</p><ul><li>并发</li><li>context</li><li>工程组织</li><li>性能分析</li></ul><h3 id="11-4-方向四：工具链与效率工具">11.4 方向四：工具链与效率工具</h3><p>适合你如果：</p><ul><li>喜欢 CLI 工具</li><li>喜欢自动化</li><li>喜欢开发者效率方向</li></ul><p>建议下一步重点学：</p><ul><li>命令行框架</li><li>文件系统处理</li><li>并发任务调度</li><li>插件化思路</li><li>终端输出和用户体验</li></ul><p>这条路线很适合 Go，也很容易做出可展示的作品。</p><hr><h2 id="12-如果你不知道选哪个方向，怎么判断">12. 如果你不知道选哪个方向，怎么判断</h2><p>一个很实用的办法是看你更喜欢解决哪类问题。</p><h3 id="12-1-你更喜欢业务流程">12.1 你更喜欢业务流程</h3><p>比如：</p><ul><li>用户、订单、支付、权限</li></ul><p>那你更适合：</p><ul><li>Web 后端</li><li>业务服务开发</li></ul><h3 id="12-2-你更喜欢系统运行和平台">12.2 你更喜欢系统运行和平台</h3><p>比如：</p><ul><li>调度</li><li>部署</li><li>监控</li><li>自动化</li></ul><p>那你更适合：</p><ul><li>云原生</li><li>基础设施</li><li>平台工程</li></ul><h3 id="12-3-你更喜欢做小而锋利的工具">12.3 你更喜欢做小而锋利的工具</h3><p>比如：</p><ul><li>命令行工具</li><li>代码生成器</li><li>数据转换器</li></ul><p>那你更适合：</p><ul><li>工具链方向</li><li>开发效率方向</li></ul><h3 id="12-4-实在拿不准怎么办">12.4 实在拿不准怎么办</h3><p>就先选：</p><ul><li>Web 后端 + 一个 CLI 工具</li></ul><p>这是当前阶段最稳的组合：</p><ul><li>一个练系统设计</li><li>一个练工具感和交付闭环</li></ul><hr><h2 id="13-后续-3-个月的建议学习节奏">13. 后续 3 个月的建议学习节奏</h2><p>课程结束后，最怕的不是“不会”，而是“停住”。</p><p>这里给你一个非常实用的 3 个月路线。</p><h3 id="13-1-第一个月：做一个完整项目">13.1 第一个月：做一个完整项目</h3><p>目标：</p><ul><li>把课程知识真正串起来</li></ul><p>要求：</p><ul><li>项目能跑</li><li>有 README</li><li>有测试</li><li>有清晰目录结构</li></ul><h3 id="13-2-第二个月：补工程化能力">13.2 第二个月：补工程化能力</h3><p>目标：</p><ul><li>把项目从“能跑”提升到“更像产品”</li></ul><p>建议补：</p><ul><li>配置管理</li><li>日志</li><li>错误语义</li><li>基础 CI</li><li>简单性能分析</li></ul><h3 id="13-3-第三个月：选一个方向加深">13.3 第三个月：选一个方向加深</h3><p>可以选：</p><ul><li>Web</li><li>微服务</li><li>云原生</li><li>工具链</li></ul><p>目标不是贪多，而是：</p><blockquote><p>至少把一个方向推进到“能做出一版像样项目”。</p></blockquote><hr><h2 id="14-如何判断自己是不是真的学会了">14. 如何判断自己是不是真的学会了</h2><p>不要只靠“我看懂了”来判断。</p><p>更有效的判断标准是：</p><h3 id="14-1-你能不能独立写出来">14.1 你能不能独立写出来</h3><p>比如：</p><ul><li>一个小型服务</li><li>一个 CLI 工具</li><li>一套测试</li></ul><h3 id="14-2-你能不能解释设计理由">14.2 你能不能解释设计理由</h3><p>比如你要能说清：</p><ul><li>为什么这样拆包</li><li>为什么接口定义在这里</li><li>为什么这里传值而不是传指针</li><li>为什么这里要加 Benchmark</li></ul><h3 id="14-3-你能不能定位问题">14.3 你能不能定位问题</h3><p>比如遇到：</p><ul><li>Bug</li><li>性能热点</li><li>并发问题</li></ul><p>你是否知道该用什么工具、从哪开始排查。</p><p>如果这三条你都逐渐做到了，那你就不是“学过 Go”，而是真的开始“会用 Go”了。</p><hr><h2 id="15-常见坑总结">15. 常见坑总结</h2><h3 id="15-1-学完课程后马上停止动手">15.1 学完课程后马上停止动手</h3><p>这是最危险的。</p><p>课程结束不是终点，而是你真正开始做项目的起点。</p><h3 id="15-2-继续囤知识，不做整合">15.2 继续囤知识，不做整合</h3><p>如果只是继续看更多零散内容，却不做完整项目，成长会明显变慢。</p><h3 id="15-3-一上来挑战过大的系统">15.3 一上来挑战过大的系统</h3><p>跨度太大通常会削弱信心，也会让你陷入大量与当前阶段不匹配的复杂度。</p><h3 id="15-4-只做功能，不补工程能力">15.4 只做功能，不补工程能力</h3><p>项目不只是“功能能跑”，还包括：</p><ul><li>测试</li><li>日志</li><li>错误处理</li><li>文档</li><li>性能意识</li></ul><h3 id="15-5-只关注框架，不继续理解语言本身">15.5 只关注框架，不继续理解语言本身</h3><p>框架能让你更快开发，但语言底层认知才决定你走得多远。</p><h3 id="15-6-不复盘">15.6 不复盘</h3><p>每做完一个项目，如果不总结：</p><ul><li>哪些设计做对了</li><li>哪些地方做乱了</li><li>哪些性能问题值得优化</li></ul><p>那成长速度会慢很多。</p><hr><h2 id="16-本课练习">16. 本课练习</h2><h3 id="练习-1：设计你的综合项目">练习 1：设计你的综合项目</h3><p>从下面方向中选一个：</p><ul><li>HTTP 服务</li><li>CLI 工具</li><li>文件处理工具</li></ul><p>写出你的项目设计草图，包括：</p><ul><li>核心功能</li><li>目录结构</li><li>关键数据结构</li><li>主要模块职责</li></ul><hr><h3 id="练习-2：列一张能力映射表">练习 2：列一张能力映射表</h3><p>把前面课程中的知识点映射到你的项目里，例如：</p><ul><li>JSON -&gt; 请求与响应</li><li>context -&gt; 超时控制</li><li>Benchmark -&gt; 热点函数测试</li><li><code>pprof</code> -&gt; 性能定位</li></ul><p>写出至少 10 个映射点。</p><hr><h3 id="练习-3：选一个进阶方向">练习 3：选一个进阶方向</h3><p>从下面四个方向中选一个：</p><ul><li>Web 后端</li><li>微服务</li><li>云原生</li><li>工具链</li></ul><p>然后写出：</p><ul><li>为什么选它</li><li>接下来 1 个月打算补什么</li><li>准备做什么项目验证</li></ul><hr><h3 id="练习-4：给你的项目加工程化要求">练习 4：给你的项目加工程化要求</h3><p>为你要做的综合项目，写一份最小工程要求清单：</p><ul><li>有 README</li><li>有测试</li><li>有配置入口</li><li>有日志</li><li>有错误语义</li><li>有一个性能观察点</li></ul><p>然后逐项实现。</p><hr><h3 id="练习-5：做一次课程总复盘">练习 5：做一次课程总复盘</h3><p>用你自己的话回答：</p><ol><li>这 40 课里对你最有帮助的 5 个主题是什么</li><li>哪 3 个主题你还不够扎实</li><li>你准备如何补足</li></ol><hr><h2 id="17-自测题">17. 自测题</h2><h3 id="17-1-概念题">17.1 概念题</h3><ol><li>为什么说课程后半段最重要的任务是“做整合”，而不是继续刷零散知识点？</li><li>一个适合当前阶段的综合 Go 项目，应该具备哪些基本特征？</li><li>为什么推荐先做“完整但可控”的项目，而不是一上来做超大系统？</li><li>在一个综合项目中，输入层、业务层、存储层分别应该承担什么职责？</li><li>为什么说作品比继续囤教程更能推动成长？</li><li>Web、微服务、云原生、工具链这四个方向，适用的人群有何差异？</li><li>为什么项目完成后还要做复盘？</li><li>怎样判断自己已经从“学过 Go”进入“会用 Go”的阶段？</li></ol><h3 id="17-2-场景题">17.2 场景题</h3><p>如果你现在已经学完这 40 课，但还没有做过完整项目，下面哪种做法更合理？为什么？</p><figure class="highlight text"><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">方案 A：继续看 50 篇零散 Go 技巧文章。</span><br><span class="line">方案 B：做一个完整的小型 Go HTTP 服务，并补上测试、日志、配置和性能观察。</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>更合理的是 <strong>方案 B</strong>。</p><p>原因是：</p><ol><li>你当前阶段最需要的是把分散知识串起来，形成完整工程能力</li><li>一个完整项目会逼你把语法、标准库、并发、测试、工程组织、性能分析真正落地</li><li>继续刷大量零散技巧文章，边际收益已经开始下降</li><li>做项目还能暴露真实短板，让你后续学习更有方向</li></ol><p>这并不是说技巧文章没价值，而是说：</p><p><strong>在当前阶段，整合能力比继续堆新知识更重要。</strong></p></details><hr><h2 id="18-本课总结">18. 本课总结</h2><p>这一课是整套课程的收束，但更准确地说，它是你真正进入 Go 实战阶段的起点。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>当前能力</td><td>已具备语言、标准库、并发、工程与高级理解基础</td></tr><tr><td>下一步重点</td><td>做完整项目，把知识串成能力</td></tr><tr><td>综合项目要求</td><td>能跑、结构清晰、有测试、有日志、有配置、有复盘</td></tr><tr><td>后续方向</td><td>Web、微服务、云原生、工具链</td></tr><tr><td>长期成长关键</td><td>多做作品、持续复盘、按方向深挖</td></tr></tbody></table><p>最重要的五件事：</p><ol><li><strong>学完课程后，最重要的事不是继续囤知识，而是做完整项目</strong></li><li><strong>项目要完整，但规模要可控</strong></li><li><strong>工程能力和功能能力同样重要</strong></li><li><strong>方向选择不必追求一步到位，先选一个持续推进比犹豫更重要</strong></li><li><strong>真正的进阶，来自反复实践、复盘和迭代</strong></li></ol><hr><h2 id="19-课程结束说明">19. 课程结束说明</h2><p>到这里，整套《Go 从 0 到精通》40 课就完整结束了。</p><p>如果把这套课程压缩成一句话，它真正想帮你建立的是：</p><blockquote><p>从“会写 Go 语法”，走到“能独立组织 Go 项目，并知道如何继续进阶”。</p></blockquote><p>后面你无论往哪个方向走，只要继续坚持这三件事，成长都会很稳：</p><ol><li>持续做项目</li><li>持续读优秀代码</li><li>持续用测试和性能工具验证自己的判断</li></ol><p>这套课程到这里收束，但 Go 的学习路线，才刚刚开始。</p>]]></content>
    
    
    <summary type="html">Go语言系列40</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 39 课：阅读标准库与源码思维训练</title>
    <link href="https://yjyrichard.github.io/posts/30bb0a06.html"/>
    <id>https://yjyrichard.github.io/posts/30bb0a06.html</id>
    <published>2026-03-22T15:28:11.777Z</published>
    <updated>2026-03-22T15:40:40.049Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 39 课：阅读标准库与源码思维训练</h1><blockquote><p>学习定位：这是整套 Go 教程的第 39 课，也是阶段六（高级进阶阶段）的第六课。<br>前置要求：已经完成第 38 课，理解 Go 的性能分析、泛型、反射、逃逸分析与核心数据结构底层行为。<br>本课目标：建立阅读 Go 标准库和优秀源码的基本方法，学会从文档、示例、测试、公开 API、内部实现这几个层次逐步进入源码，掌握“先看设计，再看细节”的阅读顺序，并能把标准库中的接口设计、错误处理、命名方式、零值可用、分层思路迁移到自己的项目中。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>到现在，你已经掌握了 Go 的语法、标准库常用能力、并发、测试、Benchmark、性能分析和一些底层认知。</p><p>接下来真正拉开水平差距的，不再只是“你会不会写语法”，而是：</p><ul><li>你能不能读懂别人写的 Go 代码</li><li>你能不能从优秀实现里学设计</li><li>你能不能把这些思路迁移到自己的项目里</li></ul><p>很多人学语言会卡在这里：</p><ul><li>API 会用，但不会读实现</li><li>遇到复杂源码就头大</li><li>一打开标准库就觉得“太多了，不知道从哪开始”</li><li>看了半小时，最后只记住几行细节，没有学到方法</li></ul><p>这节课就是要解决这个问题。</p><p><strong>阅读标准库，不是为了把源码背下来，而是为了训练“工程判断力”。</strong></p><p>你真正要学会的是：</p><ol><li>怎么挑一段源码开始读</li><li>先读什么，后读什么</li><li>怎么从源码里提炼可复用的设计原则</li></ol><hr><h2 id="2-为什么阅读标准库是进阶-Go-的关键动作">2. 为什么阅读标准库是进阶 Go 的关键动作</h2><p>很多语言生态里，框架比标准库更重要。</p><p>但 Go 有一个很鲜明的特点：</p><ul><li>标准库覆盖面广</li><li>风格统一</li><li>工程质量高</li><li>设计取舍很典型</li></ul><p>所以阅读标准库的价值非常高。</p><h3 id="2-1-你会学到“Go-风格”的真实落地方式">2.1 你会学到“Go 风格”的真实落地方式</h3><p>比如：</p><ul><li>包怎么命名</li><li>接口怎么设计得小而清晰</li><li>错误怎么逐层返回</li><li>零值怎么做到可用</li><li>文档和代码怎么相互配合</li></ul><p>这些东西，标准库比大多数教程更有说服力。</p><h3 id="2-2-你会开始理解“为什么-API-长这样”">2.2 你会开始理解“为什么 API 长这样”</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></pre></td><td class="code"><pre><span class="line">strings.Builder&#123;&#125;</span><br><span class="line">json.Unmarshal(...)</span><br><span class="line">http.NewRequest(...)</span><br><span class="line">context.WithTimeout(...)</span><br></pre></td></tr></table></figure><p>现在你会开始思考：</p><ul><li>为什么它用这种函数签名</li><li>为什么它返回这组值</li><li>为什么某些能力用接口表达</li><li>为什么错误处理方式是这样的</li></ul><p>这就是从“会调用”进入“会设计”的开始。</p><h3 id="2-3-你会训练自己读复杂代码的能力">2.3 你会训练自己读复杂代码的能力</h3><p>标准库源码通常比学习示例更真实：</p><ul><li>文件更多</li><li>调用链更长</li><li>边界处理更完整</li></ul><p>如果你能把标准库慢慢读顺，以后读业务框架、开源库、公司内部基础设施，都会轻松很多。</p><hr><h2 id="3-阅读源码之前，先建立正确目标">3. 阅读源码之前，先建立正确目标</h2><p>这是最重要的前置认知。</p><h3 id="3-1-错误目标：一上来想“全部看懂”">3.1 错误目标：一上来想“全部看懂”</h3><p>很多人一打开源码就想：</p><blockquote><p>我要把这个包从第一行到最后一行全部吃透。</p></blockquote><p>这通常只会导致：</p><ul><li>信息过载</li><li>很快疲劳</li><li>读完没有形成结构</li></ul><h3 id="3-2-正确目标：一次只解决一个问题">3.2 正确目标：一次只解决一个问题</h3><p>例如你可以带着具体问题去读：</p><ul><li><code>strings.Builder</code> 为什么比 <code>+</code> 拼接更高效？</li><li><code>context.WithCancel</code> 为什么要返回 <code>cancel</code> 函数？</li><li><code>io.Reader</code> 为什么只定义一个 <code>Read</code> 方法？</li><li><code>sort.Search</code> 的函数签名为什么设计成这样？</li></ul><p>这样读源码，效率会高很多。</p><h3 id="3-3-源码阅读的本质不是“背实现”，而是“学设计”">3.3 源码阅读的本质不是“背实现”，而是“学设计”</h3><p>你真正要带走的是：</p><ul><li>设计边界</li><li>抽象方式</li><li>命名习惯</li><li>错误处理策略</li><li>性能与可读性的平衡</li></ul><p>这些东西才是可迁移的。</p><hr><h2 id="4-新手最稳的阅读顺序">4. 新手最稳的阅读顺序</h2><p>阅读源码最怕顺序错了。</p><p>如果你一上来就扎进实现细节，往往很快就迷路。</p><p>一个非常稳的顺序是：</p><ol><li>先看包文档</li><li>再看导出的类型和函数</li><li>再看一个最常见使用路径</li><li>再看测试和示例</li><li>最后再进入内部实现</li></ol><p>这五步非常重要。</p><p>你可以把它理解成：</p><blockquote><p>先建立地图，再进街区；先看骨架，再看肌肉。</p></blockquote><hr><h2 id="5-第一步：先看包文档，不要急着翻源码">5. 第一步：先看包文档，不要急着翻源码</h2><p>比如你想读 <code>strings</code> 包，先不要马上打开实现文件。</p><p>先问自己：</p><ul><li>这个包解决什么问题？</li><li>核心类型有哪些？</li><li>最重要的函数有哪些？</li><li>它面向什么使用场景？</li></ul><p>包文档会先给你这些信息。</p><h3 id="5-1-为什么这一步重要">5.1 为什么这一步重要</h3><p>因为文档其实就是作者给你的“阅读导航”。</p><p>它会告诉你：</p><ul><li>包的定位</li><li>核心概念</li><li>主要入口</li></ul><p>如果跳过这一层，你就容易变成“看到了很多代码，但不知道它们在系统里扮演什么角色”。</p><h3 id="5-2-读文档时要重点看什么">5.2 读文档时要重点看什么</h3><ul><li>包一句话描述</li><li>公开类型</li><li>公开函数</li><li>示例代码</li><li>备注说明和限制条件</li></ul><p>这一步做完，你心里应该至少能回答：</p><blockquote><p>这个包最重要的 20% 内容是什么？</p></blockquote><hr><h2 id="6-第二步：先读公开-API，而不是内部细节">6. 第二步：先读公开 API，而不是内部细节</h2><p>标准库最值得学习的，不只是实现，更是 API 设计。</p><h3 id="6-1-先看导出内容">6.1 先看导出内容</h3><p>比如一个包里你先看：</p><ul><li>导出的结构体</li><li>导出的接口</li><li>导出的构造函数</li><li>导出的方法</li></ul><p>例如读 <code>strings.Builder</code>，你会先关注：</p><ul><li><code>type Builder struct</code></li><li><code>WriteString</code></li><li><code>WriteByte</code></li><li><code>String</code></li><li><code>Grow</code></li><li><code>Reset</code></li></ul><h3 id="6-2-为什么这一步比看内部逻辑更重要">6.2 为什么这一步比看内部逻辑更重要</h3><p>因为对于工程设计来说，最核心的问题不是：</p><blockquote><p>它内部用了几个变量？</p></blockquote><p>而是：</p><blockquote><p>它把什么能力暴露给调用者？边界怎么划？用起来顺不顺？</p></blockquote><p>这才是你以后写自己包时真正会复用的地方。</p><h3 id="6-3-一个实用问题清单">6.3 一个实用问题清单</h3><p>看到一个 API，你可以问：</p><ol><li>为什么函数名叫这个？</li><li>为什么参数顺序这样排？</li><li>为什么返回值是这一组？</li><li>为什么这个能力做成方法，而不是普通函数？</li><li>为什么有些字段/类型不导出？</li></ol><p>这些问题会逼你从“使用者视角”进入“设计者视角”。</p><hr><h2 id="7-第三步：先找一条最常见调用路径">7. 第三步：先找一条最常见调用路径</h2><p>读源码最怕“到处翻，但没主线”。</p><p>所以你最好先选一条最常见的调用路径。</p><h3 id="7-1-例子：读-strings-Builder">7.1 例子：读 <code>strings.Builder</code></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> b strings.Builder</span><br><span class="line">b.WriteString(<span class="string">&quot;Go&quot;</span>)</span><br><span class="line">b.WriteString(<span class="string">&quot;语言&quot;</span>)</span><br><span class="line">fmt.Println(b.String())</span><br></pre></td></tr></table></figure><p>这时你就只追一条主线：</p><ul><li><code>Builder</code> 长什么样</li><li><code>WriteString</code> 做了什么</li><li><code>String</code> 怎么返回结果</li></ul><p>不要一开始就把这个包所有函数都展开。</p><h3 id="7-2-例子：读-context">7.2 例子：读 <code>context</code></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></pre></td><td class="code"><pre><span class="line">ctx, cancel := context.WithTimeout(context.Background(), time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br></pre></td></tr></table></figure><p>此时你只关心：</p><ul><li>为什么返回两个值</li><li><code>cancel</code> 到底负责什么</li><li>上下文取消是如何向下传播的</li></ul><p>主线清楚后，你读起来就会稳定很多。</p><hr><h2 id="8-第四步：一定要读测试和示例">8. 第四步：一定要读测试和示例</h2><p>这一步经常被忽略，但非常重要。</p><h3 id="8-1-测试是“行为说明书”">8.1 测试是“行为说明书”</h3><p>很多时候，测试比实现更适合先读。</p><p>因为测试告诉你：</p><ul><li>作者希望这个包怎么被使用</li><li>边界情况有哪些</li><li>哪些行为是明确承诺的</li></ul><h3 id="8-2-示例是“最短使用路径”">8.2 示例是“最短使用路径”</h3><p>示例代码通常会告诉你：</p><ul><li>最常见的正确用法</li><li>API 的推荐姿势</li></ul><p>这能帮你快速建立上下文。</p><h3 id="8-3-为什么测试对源码阅读特别重要">8.3 为什么测试对源码阅读特别重要</h3><p>因为实现细节可能很复杂，但测试里体现的是：</p><p><strong>对外行为。</strong></p><p>而工程上最重要的，往往正是：</p><ul><li>输入是什么</li><li>输出是什么</li><li>边界怎么处理</li></ul><p>这也是你以后写自己库时很值得学习的一点。</p><hr><h2 id="9-第五步：最后才进入内部实现">9. 第五步：最后才进入内部实现</h2><p>前四步做完后，再看实现，就不会那么容易迷失。</p><p>这时你主要看三类东西：</p><ol><li>数据结构怎么组织</li><li>核心路径怎么走</li><li>边界和错误怎么处理</li></ol><h3 id="9-1-不要试图第一次就把所有分支都看完">9.1 不要试图第一次就把所有分支都看完</h3><p>很多标准库实现会包含：</p><ul><li>快速路径</li><li>慢速路径</li><li>特殊 case</li><li>平台差异</li><li>优化分支</li></ul><p>第一次读时，你只要先抓住：</p><ul><li>主流程</li><li>关键数据结构</li><li>为什么这样设计</li></ul><p>就够了。</p><h3 id="9-2-一个非常实用的阅读方法">9.2 一个非常实用的阅读方法</h3><p>你可以把每个函数先归类：</p><ul><li>入口函数</li><li>参数校验</li><li>真实核心逻辑</li><li>辅助函数</li><li>错误处理/收尾逻辑</li></ul><p>这样你就不会被一堆细节函数拖着走。</p><hr><h2 id="10-一个例子：从-io-Reader-学接口设计">10. 一个例子：从 <code>io.Reader</code> 学接口设计</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Reader <span class="keyword">interface</span> &#123;</span><br><span class="line">    Read(p []<span class="type">byte</span>) (n <span class="type">int</span>, err <span class="type">error</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码极短，但非常值得反复看。</p><h3 id="10-1-为什么只定义一个方法">10.1 为什么只定义一个方法</h3><p>因为接口的目标不是“功能越多越好”，而是：</p><p><strong>抓住最小必要行为。</strong></p><p>只要一个类型能把数据读进 <code>[]byte</code>，它就可以被当成 <code>Reader</code>。</p><h3 id="10-2-它体现了什么设计思想">10.2 它体现了什么设计思想</h3><ol><li>小接口</li><li>行为抽象</li><li>解耦具体实现</li></ol><p>于是：</p><ul><li>文件可以是 <code>Reader</code></li><li>网络连接可以是 <code>Reader</code></li><li>内存缓冲可以是 <code>Reader</code></li></ul><p>这就是标准库最经典的抽象风格之一。</p><h3 id="10-3-你该从中学到什么">10.3 你该从中学到什么</h3><p>以后你设计接口时，不要先问：</p><blockquote><p>我能不能把所有需求都塞进一个接口？</p></blockquote><p>而要先问：</p><blockquote><p>这个行为最小可以抽象到什么程度？</p></blockquote><hr><h2 id="11-一个例子：从-strings-Builder-学零值可用和性能意识">11. 一个例子：从 <code>strings.Builder</code> 学零值可用和性能意识</h2><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">var</span> b strings.Builder</span><br><span class="line">b.WriteString(<span class="string">&quot;Go&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="11-1-第一件值得学的事：零值可用">11.1 第一件值得学的事：零值可用</h3><p>你不需要：</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">b := <span class="built_in">new</span>(strings.Builder)</span><br></pre></td></tr></table></figure><p>也不需要某个初始化函数。</p><p>直接零值就能用。</p><p>这体现了 Go 非常重要的一种设计追求：</p><blockquote><p>一个类型如果可能，尽量让零值就代表可用状态。</p></blockquote><h3 id="11-2-第二件值得学的事：能力聚焦">11.2 第二件值得学的事：能力聚焦</h3><p><code>Builder</code> 没有试图变成“万能字符串工具箱”。</p><p>它只专注一件事：</p><ul><li>高效构建字符串</li></ul><p>这说明标准库类型通常职责都很克制。</p><h3 id="11-3-第三件值得学的事：API-和性能相互呼应">11.3 第三件值得学的事：API 和性能相互呼应</h3><p>像：</p><ul><li><code>Grow</code></li><li><code>WriteString</code></li><li><code>Reset</code></li></ul><p>这些 API 不只是功能设计，也是在暴露性能控制点。</p><p>这类设计很值得在自己的工具库里借鉴。</p><hr><h2 id="12-一个例子：从-context-学“控制流”设计">12. 一个例子：从 <code>context</code> 学“控制流”设计</h2><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">ctx, cancel := context.WithTimeout(parent, time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br></pre></td></tr></table></figure><h3 id="12-1-为什么这个-API-值得读">12.1 为什么这个 API 值得读</h3><p>因为它不只是功能 API，它体现的是一套很清晰的控制流思想：</p><ul><li>上下文是树状传播的</li><li>取消是显式的</li><li>生命周期由调用方负责管理</li></ul><h3 id="12-2-从源码和-API-里能学到什么">12.2 从源码和 API 里能学到什么</h3><ol><li>资源释放不要藏着做，最好显式交给调用方</li><li>父子关系要清晰</li><li>控制能力和业务逻辑尽量解耦</li></ol><p>这类思路对你以后设计：</p><ul><li>任务取消</li><li>超时控制</li><li>请求链上下文</li></ul><p>都很有帮助。</p><hr><h2 id="13-一个例子：从-sort-学函数式参数设计">13. 一个例子：从 <code>sort</code> 学函数式参数设计</h2><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></pre></td><td class="code"><pre><span class="line">sort.Slice(users, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> users[i].Age &lt; users[j].Age</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="13-1-这个-API-为什么值得学">13.1 这个 API 为什么值得学</h3><p>它体现了 Go 很常见的一种设计：</p><ul><li>标准库提供稳定通用框架</li><li>调用方通过函数参数注入局部规则</li></ul><p>这里标准库不需要知道：</p><ul><li><code>users</code> 的业务含义是什么</li><li>年龄为什么排序</li></ul><p>它只需要一个“比较规则”。</p><h3 id="13-2-你该学到什么">13.2 你该学到什么</h3><p>这类设计非常适合：</p><ul><li>排序规则</li><li>过滤条件</li><li>回调处理</li><li>钩子行为</li></ul><p>也就是：</p><p><strong>把变化点收敛成函数参数，而不是把所有逻辑写死在库里。</strong></p><hr><h2 id="14-阅读源码时，优先学哪些东西">14. 阅读源码时，优先学哪些东西</h2><p>并不是源码里所有内容都同等重要。</p><p>对于当前阶段，你最该优先学的是这五类东西。</p><h3 id="14-1-接口设计">14.1 接口设计</h3><p>重点看：</p><ul><li>为什么接口这么小</li><li>为什么定义在这里</li><li>为什么不是另外一种抽象方式</li></ul><h3 id="14-2-错误处理">14.2 错误处理</h3><p>重点看：</p><ul><li>错误是直接返回还是包装</li><li>哪些情况返回 sentinel error</li><li>哪些情况返回具体上下文</li></ul><h3 id="14-3-零值可用">14.3 零值可用</h3><p>重点看：</p><ul><li>哪些类型零值就能用</li><li>作者为了零值可用做了什么约束</li></ul><h3 id="14-4-命名和分层">14.4 命名和分层</h3><p>重点看：</p><ul><li>包名为什么这么短</li><li>类型名和方法名为什么这样搭配</li><li>内部辅助函数如何组织</li></ul><h3 id="14-5-快速路径和慢速路径">14.5 快速路径和慢速路径</h3><p>重点看：</p><ul><li>哪些地方先走简单快速路径</li><li>哪些场景才进入复杂处理</li></ul><p>这对你理解性能和工程取舍很有帮助。</p><hr><h2 id="15-源码看不懂时，怎么拆解">15. 源码看不懂时，怎么拆解</h2><p>这件事每个人都会遇到，不要把“第一次看不懂”当成自己水平不够。</p><h3 id="15-1-先把问题缩小">15.1 先把问题缩小</h3><p>不要问：</p><blockquote><p>这个包我为什么看不懂？</p></blockquote><p>要改成：</p><blockquote><p>我现在卡在这个函数的哪一步？</p></blockquote><p>比如：</p><ul><li>是函数签名没看懂</li><li>是某个结构体字段不知道用途</li><li>是调用链太长</li><li>是某个辅助函数不知道为什么存在</li></ul><p>问题一旦变具体，就能解。</p><h3 id="15-2-先画出调用主线">15.2 先画出调用主线</h3><p>比如从入口函数开始，只记：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">A -&gt; B -&gt; C -&gt; D</span><br></pre></td></tr></table></figure><p>先不要追每个分支。</p><p>先知道：</p><ul><li>主流程怎么走</li><li>哪些是关键节点</li></ul><h3 id="15-3-先跳过优化分支">15.3 先跳过优化分支</h3><p>很多源码里会有：</p><ul><li>特殊 case</li><li>平台分支</li><li>内联优化</li><li>边缘处理</li></ul><p>第一次看不懂时，完全可以先跳过，只抓主干逻辑。</p><h3 id="15-4-先写一个最小使用例子">15.4 先写一个最小使用例子</h3><p>这通常特别有效。</p><p>比如你读某个标准库时，自己写一个 10 行小程序去调用它，再对照源码看，理解速度会快很多。</p><p>因为此时：</p><ul><li>你知道输入是什么</li><li>你知道输出是什么</li><li>你知道自己正在追哪条路径</li></ul><hr><h2 id="16-阅读源码时，怎么做笔记最有效">16. 阅读源码时，怎么做笔记最有效</h2><p>如果只是“看过去”，通常很快就忘。</p><p>更有效的方式是做结构化笔记。</p><h3 id="16-1-推荐记这四类内容">16.1 推荐记这四类内容</h3><ol><li>包的核心职责</li><li>最重要的入口 API</li><li>关键数据结构</li><li>自己学到的设计点</li></ol><h3 id="16-2-一个简单模板">16.2 一个简单模板</h3><p>你可以记成这样：</p><figure class="highlight text"><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">包名：strings</span><br><span class="line">核心职责：字符串处理</span><br><span class="line">关键入口：Builder、Split、Join、Contains</span><br><span class="line">设计点：</span><br><span class="line">- Builder 零值可用</span><br><span class="line">- API 命名短而直接</span><br><span class="line">- 高频操作尽量提供专门方法</span><br></pre></td></tr></table></figure><h3 id="16-3-为什么别记太多实现细节">16.3 为什么别记太多实现细节</h3><p>因为：</p><ul><li>很多实现细节以后会忘</li><li>版本也可能变化</li></ul><p>而真正长期有价值的是：</p><ul><li>设计原则</li><li>阅读路径</li><li>可迁移思路</li></ul><hr><h2 id="17-怎样把“读懂源码”迁移到自己的项目">17. 怎样把“读懂源码”迁移到自己的项目</h2><p>这是整节课最重要的落点。</p><p>你读标准库，不是为了变成“标准库讲解员”，而是为了提升自己写代码的质量。</p><h3 id="17-1-迁移点一：小接口">17.1 迁移点一：小接口</h3><p>从 <code>io.Reader</code> 这类设计里学到：</p><ul><li>接口尽量小</li><li>只抽象稳定行为</li></ul><h3 id="17-2-迁移点二：零值可用">17.2 迁移点二：零值可用</h3><p>从 <code>strings.Builder</code> 这类设计里学到：</p><ul><li>如果一个类型可以零值可用，就尽量做到</li></ul><h3 id="17-3-迁移点三：错误边界清晰">17.3 迁移点三：错误边界清晰</h3><p>从标准库大量 API 里学到：</p><ul><li>参数错就尽早返回</li><li>错误要清楚表达边界</li></ul><h3 id="17-4-迁移点四：职责克制">17.4 迁移点四：职责克制</h3><p>从很多标准库包里学到：</p><ul><li>一个包不要什么都想管</li><li>一个类型不要什么都想做</li></ul><h3 id="17-5-迁移点五：文档和示例同样重要">17.5 迁移点五：文档和示例同样重要</h3><p>标准库的很多包之所以好用，不只是代码写得好，也是因为：</p><ul><li>文档清楚</li><li>示例直达</li><li>命名稳定</li></ul><p>这对你以后写自己项目里的公共包也一样重要。</p><hr><h2 id="18-初学阶段，推荐读哪些标准库">18. 初学阶段，推荐读哪些标准库</h2><p>不是所有包都适合作为第一批源码阅读对象。</p><h3 id="18-1-第一梯队：最适合入门">18.1 第一梯队：最适合入门</h3><ul><li><code>strings</code></li><li><code>bytes</code></li><li><code>sort</code></li><li><code>strconv</code></li><li><code>errors</code></li><li><code>io</code></li></ul><p>这些包通常：</p><ul><li>目标清晰</li><li>规模适中</li><li>设计点多</li><li>不容易一上来把人看晕</li></ul><h3 id="18-2-第二梯队：适合建立工程视角">18.2 第二梯队：适合建立工程视角</h3><ul><li><code>context</code></li><li><code>bufio</code></li><li><code>flag</code></li><li><code>net/http</code></li></ul><p>这些包更适合训练：</p><ul><li>接口设计</li><li>分层思维</li><li>生命周期管理</li></ul><h3 id="18-3-暂时不要一上来硬啃的">18.3 暂时不要一上来硬啃的</h3><p>对于当前阶段，不建议一开始就把精力放在：</p><ul><li><code>runtime</code></li><li><code>net</code></li><li><code>syscall</code></li><li><code>reflect</code> 的深层实现</li></ul><p>不是说不能看，而是它们作为第一批材料，性价比通常不高。</p><hr><h2 id="19-一个推荐的源码阅读流程模板">19. 一个推荐的源码阅读流程模板</h2><p>以后你可以把这套流程反复复用。</p><h3 id="19-1-模板">19.1 模板</h3><ol><li>先写下你为什么要读这个包</li><li>先看包文档和示例</li><li>列出 3～5 个核心导出 API</li><li>选一条最常见使用路径</li><li>读对应测试</li><li>再进入实现主线</li><li>记录 2～3 个值得迁移的设计点</li></ol><h3 id="19-2-为什么这套模板有效">19.2 为什么这套模板有效</h3><p>因为它保证你不是“无目标乱翻”，而是：</p><ul><li>有问题驱动</li><li>有路径</li><li>有输出</li></ul><p>真正有效的源码阅读，最后一定应该产出：</p><ul><li>一个理解结果</li><li>一组可迁移经验</li></ul><p>而不是“我今天看了 500 行源码”这种没有沉淀的努力。</p><hr><h2 id="20-常见坑总结">20. 常见坑总结</h2><h3 id="20-1-一上来就读最底层实现">20.1 一上来就读最底层实现</h3><p>这样最容易迷路。</p><p>先读文档、公开 API、测试，再下钻。</p><h3 id="20-2-把源码阅读变成“逐行翻译”">20.2 把源码阅读变成“逐行翻译”</h3><p>如果你读完只能复述：</p><ul><li>这里定义了变量</li><li>那里调用了函数</li></ul><p>却说不出设计意图，那说明还没读到重点。</p><h3 id="20-3-不带问题去读">20.3 不带问题去读</h3><p>没有问题驱动时，源码很容易变成信息噪音。</p><h3 id="20-4-只看实现，不看测试和示例">20.4 只看实现，不看测试和示例</h3><p>这样很容易错过：</p><ul><li>推荐用法</li><li>边界条件</li><li>作者真实承诺的行为</li></ul><h3 id="20-5-一次看太多包">20.5 一次看太多包</h3><p>源码阅读也需要节奏。</p><p>一次只挑一个包、一个主题，效果往往更好。</p><h3 id="20-6-把标准库设计生搬硬套到业务代码">20.6 把标准库设计生搬硬套到业务代码</h3><p>标准库的抽象层次和业务项目不完全一样。</p><p>你应该学的是：</p><ul><li>思想</li><li>原则</li><li>取舍方式</li></ul><p>而不是机械照搬形状。</p><hr><h2 id="21-本课练习">21. 本课练习</h2><h3 id="练习-1：读-strings-Builder">练习 1：读 <code>strings.Builder</code></h3><p>按本课流程完成一次阅读：</p><ol><li>先看文档和示例</li><li>列出核心方法</li><li>找一条最常见使用路径</li><li>总结你学到的 3 个设计点</li></ol><hr><h3 id="练习-2：读-io-包中的-Reader">练习 2：读 <code>io</code> 包中的 <code>Reader</code></h3><p>回答以下问题：</p><ul><li>为什么接口只有一个方法？</li><li>为什么这个抽象可以支持那么多实现？</li><li>这种“小接口”思想对你自己的项目有什么启发？</li></ul><hr><h3 id="练习-3：读-sort-Slice">练习 3：读 <code>sort.Slice</code></h3><p>尝试总结：</p><ul><li>为什么比较逻辑通过函数传入</li><li>这种设计和“写死排序规则”相比有什么优点</li><li>这种思路还能迁移到哪些业务场景</li></ul><hr><h3 id="练习-4：读一个你常用但没看过实现的包">练习 4：读一个你常用但没看过实现的包</h3><p>要求：</p><ul><li>先写出“我为什么读它”</li><li>列出 3 个公开 API</li><li>选一条调用主线</li><li>记录 2 个设计启发</li></ul><hr><h3 id="练习-5：把学到的一个设计点迁移到自己项目">练习 5：把学到的一个设计点迁移到自己项目</h3><p>例如：</p><ul><li>把一个大接口拆成小接口</li><li>把某个类型改成零值可用</li><li>改善某个公共包的命名和方法组织</li></ul><p>并说明你参考的是标准库里的哪个思路。</p><hr><h2 id="22-自测题">22. 自测题</h2><h3 id="22-1-概念题">22.1 概念题</h3><ol><li>阅读标准库最重要的目标是什么？</li><li>为什么源码阅读不适合一上来就追求“全部看懂”？</li><li>为什么推荐先看文档、公开 API、测试，再看实现？</li><li>从 <code>io.Reader</code> 这类设计里最值得学到的是什么？</li><li>为什么说测试文件往往是很好的行为说明书？</li><li>看不懂源码时，为什么先抓主流程比追所有分支更有效？</li><li>为什么源码阅读要输出“可迁移设计点”，而不只是实现细节？</li><li>标准库思路为什么不能机械照搬到业务代码？</li></ol><h3 id="22-2-代码阅读题">22.2 代码阅读题</h3><p>假设你正在读一个包，里面公开暴露了下面这些 API：</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="keyword">type</span> Builder <span class="keyword">struct</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *Builder)</span></span> WriteString(s <span class="type">string</span>) &#123;&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *Builder)</span></span> Reset() &#123;&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *Builder)</span></span> String() <span class="type">string</span> &#123;&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *Builder)</span></span> Grow(n <span class="type">int</span>) &#123;&#125;</span><br></pre></td></tr></table></figure><p>不看内部实现，只从 API 设计上，你能推测出哪些信息？</p><details><summary>点击查看答案</summary><p>至少可以推测出这些点：</p><p><strong>第一，它大概率是一个“可复用的构建器类型”。</strong></p><ul><li>名字叫 <code>Builder</code></li><li>有写入方法、有结果方法、有重置方法</li></ul><p><strong>第二，它的核心职责很聚焦。</strong></p><ul><li>所有公开方法都围绕“累积内容并生成结果”</li><li>没有暴露无关能力</li></ul><p><strong>第三，它明显考虑了性能或复用场景。</strong></p><ul><li><code>Grow(n)</code> 暗示可以提前扩容</li><li><code>Reset()</code> 暗示这个对象可以重复使用</li></ul><p><strong>第四，它的调用方式偏向状态型对象而不是一次性函数。</strong></p><ul><li>用方法而不是单个大函数</li><li>说明作者认为“逐步写入、最后取结果”是主要使用路径</li></ul><p>这就是源码阅读里很重要的能力：</p><p><strong>即使暂时不看实现，也能先从 API 反推出设计意图。</strong></p></details><hr><h2 id="23-本课总结">23. 本课总结</h2><p>这一课真正训练的，不是“你看过多少源码”，而是“你能不能用正确方法看源码”。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>阅读顺序</td><td>文档 -&gt; API -&gt; 示例/测试 -&gt; 实现</td></tr><tr><td>阅读目标</td><td>学设计，不是背细节</td></tr><tr><td>核心抓手</td><td>接口、错误、零值可用、命名、分层</td></tr><tr><td>遇阻拆解</td><td>缩小问题、画主线、跳过边缘优化分支</td></tr><tr><td>输出要求</td><td>提炼可迁移的工程思路</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>阅读源码要问题驱动，而不是无目标乱翻</strong></li><li><strong>先看对外设计，再看内部实现，理解会稳得多</strong></li><li><strong>测试和示例往往比实现更适合先建立行为认知</strong></li><li><strong>真正有价值的源码阅读，一定会沉淀成你自己的设计判断力</strong></li></ol><hr><h2 id="24-下一课预告">24. 下一课预告</h2><p>下一课就是整套课程的收束课了。我们会把前面学到的语法、标准库、并发、测试、性能、工程组织和高级认知串起来，落到一个完整的综合视角中。</p><p>下一课：<strong>综合项目与进阶路线建议</strong></p><p>会重点讲：</p><ul><li>一个综合 Go 项目应该如何设计和拆分</li><li>如何把课程里的知识点串成实际能力</li><li>学完这 40 课之后该怎么继续进阶</li><li>Web、微服务、云原生、工具链几个方向怎么选</li><li>后续自学应该如何规划节奏</li></ul><p>学完下一课，这整套 Go 课程就会完成闭环。</p>]]></content>
    
    
    <summary type="html">Go语言系列39</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 38 课：性能分析与调优入门</title>
    <link href="https://yjyrichard.github.io/posts/880838e1.html"/>
    <id>https://yjyrichard.github.io/posts/880838e1.html</id>
    <published>2026-03-22T15:28:11.773Z</published>
    <updated>2026-03-22T15:40:40.045Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 38 课：性能分析与调优入门</h1><blockquote><p>学习定位：这是整套 Go 教程的第 38 课，也是阶段六（高级进阶阶段）的第五课。<br>前置要求：已经完成第 37 课，理解 Benchmark、内存与逃逸分析、切片、map、接口底层行为。<br>本课目标：掌握 Go 性能分析的基本思路，理解 <code>pprof</code> 的作用，学会采集 CPU 和内存 Profile，能用 <code>go tool pprof</code> 查看热点函数、调用路径和内存分配情况，并建立“先定位瓶颈、再做优化、最后回归验证”的调优习惯。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>上一课你已经开始理解：</p><ul><li>为什么会分配到堆上</li><li>为什么接口、反射、切片共享会影响性能</li><li>为什么不能只靠“经验口号”判断快慢</li></ul><p>但到了真实项目里，性能问题很少长这样：</p><blockquote><p>“我明确知道第 42 行最慢，请帮我优化。”</p></blockquote><p>更多时候，情况是这样的：</p><ul><li>服务整体变慢了，但不知道慢在哪</li><li>CPU 占用高，但不知道是谁吃掉了</li><li>内存涨得快，但不知道是谁在分配</li><li>某个接口偶尔卡顿，但不知道热点路径在哪</li><li>Benchmark 能看出慢，但不知道慢在函数内部哪一段</li></ul><p>这时候，仅靠肉眼看代码通常不够。</p><p>你需要的是：</p><p><strong>让程序自己告诉你，时间和内存到底花在了哪里。</strong></p><p>这就是 <code>pprof</code> 的价值。</p><p>这一课会解决四个核心问题：</p><ol><li><code>pprof</code> 到底是什么</li><li>怎么采集 CPU 和内存分析数据</li><li>怎么读 <code>pprof</code> 报告</li><li>优化时应该遵循什么顺序，避免“瞎改”</li></ol><hr><h2 id="2-性能优化最忌讳的事：凭感觉改代码">2. 性能优化最忌讳的事：凭感觉改代码</h2><p>很多初学者做性能优化，会直接进入下面这种状态：</p><ul><li>看见 <code>map</code> 就想换成切片</li><li>看见结构体传值就想改成指针</li><li>看见循环就想上协程</li><li>看见 <code>fmt.Sprintf</code> 就想手写拼接</li></ul><p>这些动作不一定错，但如果没有定位依据，就很容易出现三种结果：</p><ol><li>改了很多，几乎没收益</li><li>局部变快了，整体没变</li><li>性能没提升，代码却更难维护了</li></ol><p>所以性能分析的第一原则是：</p><blockquote><p>先测量，再判断；先定位，再优化。</p></blockquote><p>这也是为什么你前面先学了：</p><ul><li>Benchmark</li><li>逃逸分析</li><li>底层结构</li></ul><p>现在再来学 <code>pprof</code>，就正好能串起来了。</p><hr><h2 id="3-pprof-是什么">3. <code>pprof</code> 是什么</h2><h3 id="3-1-一个简化理解">3.1 一个简化理解</h3><p>你可以把 <code>pprof</code> 理解成：</p><blockquote><p>Go 提供的一套性能分析数据采集和查看工具。</p></blockquote><p>它解决的问题不是“跑得快不快”，而是：</p><ul><li>时间主要花在哪些函数</li><li>内存主要分配在哪些地方</li><li>哪些调用路径最重</li><li>哪些热点最值得优先优化</li></ul><h3 id="3-2-它和-Benchmark-的区别">3.2 它和 Benchmark 的区别</h3><p>这两个工具都重要，但职责不同。</p><table><thead><tr><th>工具</th><th>解决什么问题</th></tr></thead><tbody><tr><td>Benchmark</td><td>某段代码整体快不快、快了多少</td></tr><tr><td><code>pprof</code></td><td>时间和内存具体花在了哪里</td></tr></tbody></table><p>你可以这样理解：</p><ul><li>Benchmark 更像“体检报告上的指标变化”</li><li><code>pprof</code> 更像“进一步看片子，找病灶位置”</li></ul><h3 id="3-3-pprof-不是只能分析-CPU">3.3 <code>pprof</code> 不是只能分析 CPU</h3><p>常见 Profile 类型包括：</p><ul><li>CPU Profile：时间主要花在哪</li><li>Heap / Memory Profile：内存主要由谁分配、谁占用</li><li>Block Profile：谁在阻塞</li><li>Mutex Profile：谁在锁竞争</li></ul><p>这一课先重点讲最常用的两个：</p><ol><li>CPU</li><li>内存</li></ol><hr><h2 id="4-先理解一个关键概念：采样">4. 先理解一个关键概念：采样</h2><h3 id="4-1-pprof-不是给每一行代码都精确计时">4.1 <code>pprof</code> 不是给每一行代码都精确计时</h3><p>很多人第一次接触 Profile，会误以为：</p><blockquote><p>它是不是像秒表一样给每个函数精确记录了总耗时？</p></blockquote><p>不是。</p><p><code>pprof</code> 很多场景下采用的是**采样（sampling）**思想。</p><p>例如 CPU Profile，可以先粗略理解为：</p><ul><li>程序运行期间，定期抽样</li><li>看当下 CPU 正在执行哪条调用栈</li><li>采样足够多后，热点就会逐渐浮现出来</li></ul><h3 id="4-2-为什么采样有意义">4.2 为什么采样有意义</h3><p>因为如果你对每个函数调用都做非常重的精确记录，工具本身就可能严重拖慢程序。</p><p>采样的好处是：</p><ul><li>开销更低</li><li>对热点定位足够有效</li><li>更适合实际工程使用</li></ul><h3 id="4-3-这意味着什么">4.3 这意味着什么</h3><p>意味着 <code>pprof</code> 给你的不是“数学上绝对精确到每一纳秒的完整记录”，而是：</p><blockquote><p>足够可靠的热点分布图。</p></blockquote><p>你做性能分析时，不要把它当成逐行真值表，而要把它当成：</p><ul><li>找热点</li><li>排优先级</li><li>定位方向</li></ul><p>的工具。</p><hr><h2 id="5-最常用的两类-Profile">5. 最常用的两类 Profile</h2><h3 id="5-1-CPU-Profile">5.1 CPU Profile</h3><p>CPU Profile 关注的是：</p><ul><li>程序运行时，CPU 时间主要消耗在哪些函数上</li></ul><p>适合场景：</p><ul><li>接口慢</li><li>计算慢</li><li>某段逻辑吞吐低</li><li>某个任务执行时间过长</li></ul><h3 id="5-2-内存-Profile">5.2 内存 Profile</h3><p>内存 Profile 关注的是：</p><ul><li>哪些函数分配了最多内存</li><li>哪些对象仍然占据大量堆空间</li></ul><p>适合场景：</p><ul><li>内存占用高</li><li>GC 压力大</li><li><code>allocs/op</code> 很高</li><li>怀疑切片、map、字符串处理造成大量分配</li></ul><h3 id="5-3-两者经常要配合看">5.3 两者经常要配合看</h3><p>例如你看到 CPU 热点里有很多：</p><ul><li><code>runtime.mallocgc</code></li><li>GC 相关函数</li></ul><p>那通常说明：</p><ul><li>问题不只是“算得慢”</li><li>还可能是“分配太多，GC 太忙”</li></ul><p>这时你就应该进一步看内存 Profile。</p><hr><h2 id="6-在测试里采集-Profile：最适合入门">6. 在测试里采集 Profile：最适合入门</h2><p>对于学习阶段，最方便的入口通常是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span></span><br></pre></td></tr></table></figure><p>因为测试和基准测试本来就方便构造稳定场景。</p><h3 id="6-1-采集-CPU-Profile">6.1 采集 CPU Profile</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -cpuprofile cpu.prof</span><br></pre></td></tr></table></figure><p>如果你想结合 Benchmark：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -run ^$ -cpuprofile cpu.prof</span><br></pre></td></tr></table></figure><p>这很适合用来分析：</p><ul><li>某组基准测试里 CPU 主要消耗在哪</li></ul><h3 id="6-2-采集内存-Profile">6.2 采集内存 Profile</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -memprofile mem.prof</span><br></pre></td></tr></table></figure><p>结合 Benchmark 也很常见：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -run ^$ -memprofile mem.prof</span><br></pre></td></tr></table></figure><h3 id="6-3-同时采集">6.3 同时采集</h3><p>根据官方 <code>runtime/pprof</code> 文档和 Go 官方博客示例，<code>go test</code> 可以直接写出 CPU 和内存 profile 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -run ^$ -cpuprofile cpu.prof -memprofile mem.prof</span><br></pre></td></tr></table></figure><p>这通常是学习和局部分析时最顺手的方式。</p><hr><h2 id="7-在独立程序里采集-Profile">7. 在独立程序里采集 Profile</h2><p>如果不是测试，而是一个独立程序，也可以手动接入 <code>runtime/pprof</code>。</p><h3 id="7-1-CPU-Profile-基本写法">7.1 CPU Profile 基本写法</h3><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="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;flag&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;runtime/pprof&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> cpuprofile = flag.String(<span class="string">&quot;cpuprofile&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;write cpu profile to file&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">    flag.Parse()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> *cpuprofile != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        f, err := os.Create(*cpuprofile)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            log.Fatal(err)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">defer</span> f.Close()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> err := pprof.StartCPUProfile(f); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            log.Fatal(err)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">defer</span> pprof.StopCPUProfile()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    run()</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">run</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// 你的业务逻辑</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go run . -cpuprofile cpu.prof</span><br></pre></td></tr></table></figure><h3 id="7-2-为什么必须-StopCPUProfile">7.2 为什么必须 <code>StopCPUProfile</code></h3><p>因为 CPU Profile 需要在程序结束前把剩余数据刷到文件里。</p><p>所以官方示例里会用：</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="keyword">defer</span> pprof.StopCPUProfile()</span><br></pre></td></tr></table></figure><p>这不是形式主义，是为了保证 profile 文件完整。</p><hr><h2 id="8-HTTP-服务里接入-pprof">8. HTTP 服务里接入 <code>pprof</code></h2><p>对于 Web 服务或长时间运行的服务程序，更常见的方式是挂出 <code>pprof</code> HTTP 端点。</p><h3 id="8-1-最简单接入方式">8.1 最简单接入方式</h3><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></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;net/http&quot;</span></span><br><span class="line">    _ <span class="string">&quot;net/http/pprof&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="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        log.Println(http.ListenAndServe(<span class="string">&quot;localhost:6060&quot;</span>, <span class="literal">nil</span>))</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 正常业务服务逻辑</span></span><br><span class="line">    <span class="keyword">select</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>引入：</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="keyword">import</span> _ <span class="string">&quot;net/http/pprof&quot;</span></span><br></pre></td></tr></table></figure><p>后，就会注册 <code>/debug/pprof/</code> 下的一组端点。</p><h3 id="8-2-常见访问方式">8.2 常见访问方式</h3><p>根据 Go 官方 <code>net/http/pprof</code> 文档和官方博客，常见命令有：</p><figure class="highlight bash"><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">go tool pprof http://localhost:6060/debug/pprof/profile</span><br><span class="line">go tool pprof http://localhost:6060/debug/pprof/heap</span><br><span class="line">go tool pprof http://localhost:6060/debug/pprof/block</span><br></pre></td></tr></table></figure><p>其中：</p><ul><li><code>/profile</code> 默认抓一段 CPU Profile</li><li><code>/heap</code> 看堆内存</li><li><code>/block</code> 看阻塞</li></ul><h3 id="8-3-一个重要安全提醒">8.3 一个重要安全提醒</h3><p><code>pprof</code> 端点通常不应该直接裸露到公网。</p><p>因为它会暴露：</p><ul><li>程序内部信息</li><li>调用栈</li><li>资源使用情况</li></ul><p>所以实际项目里通常会：</p><ul><li>只监听 <code>localhost</code></li><li>或者只在内网开放</li><li>或者加鉴权、只在排障时临时开启</li></ul><hr><h2 id="9-go-tool-pprof-怎么用">9. <code>go tool pprof</code> 怎么用</h2><p>采到 profile 文件后，下一步就是分析。</p><h3 id="9-1-最基本的命令">9.1 最基本的命令</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go tool pprof cpu.prof</span><br></pre></td></tr></table></figure><p>或者带上二进制：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go tool pprof your_binary cpu.prof</span><br></pre></td></tr></table></figure><p>对于 Go 程序，带上二进制通常能帮助符号化和定位更完整的信息。</p><h3 id="9-2-进入交互界面后，最常用的几个命令">9.2 进入交互界面后，最常用的几个命令</h3><p>你不需要一开始就学很多，先掌握这几个最有价值：</p><ul><li><code>top</code></li><li><code>list 函数名</code></li><li><code>web</code></li></ul><p>这是 Go 官方博客示例里重点展示的核心用法。</p><hr><h2 id="10-top：先看最大的热点在哪">10. <code>top</code>：先看最大的热点在哪</h2><h3 id="10-1-作用">10.1 作用</h3><p><code>top</code> 会列出最热的函数。</p><p>例如：</p><figure class="highlight text"><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">(pprof) top</span><br><span class="line">Showing nodes accounting for 2.30s, 76.67% of 3.00s total</span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">     1.20s 40.00% 40.00%      1.40s 46.67%  main.process</span><br><span class="line">     0.60s 20.00% 60.00%      0.70s 23.33%  strings.Builder.WriteString</span><br><span class="line">     0.50s 16.67% 76.67%      0.90s 30.00%  runtime.mallocgc</span><br></pre></td></tr></table></figure><h3 id="10-2-怎么读-flat-和-cum">10.2 怎么读 <code>flat</code> 和 <code>cum</code></h3><p>这是 <code>pprof</code> 最重要的两个指标。</p><table><thead><tr><th>列</th><th>含义</th></tr></thead><tbody><tr><td><code>flat</code></td><td>函数自己本身消耗的时间/资源</td></tr><tr><td><code>cum</code></td><td>函数自己加上它调用下层函数，总共消耗的时间/资源</td></tr></tbody></table><h3 id="10-3-一个直观理解">10.3 一个直观理解</h3><p>假设：</p><ul><li><code>main.handleRequest</code> 调了 <code>parse</code>、<code>queryDB</code>、<code>render</code></li></ul><p>如果 <code>handleRequest</code> 自己只负责调度，真正耗时都在下层，那它可能：</p><ul><li><code>flat</code> 不高</li><li><code>cum</code> 很高</li></ul><p>所以：</p><ul><li>看 <code>flat</code>，你更容易找到“当前真正烧 CPU 的点”</li><li>看 <code>cum</code>，你更容易找到“哪条调用链最值得继续往下钻”</li></ul><h3 id="10-4-一个经验">10.4 一个经验</h3><p>性能分析通常先看 <code>top</code>，再决定下一步钻哪几个函数。</p><hr><h2 id="11-list：下钻到具体函数和代码行">11. <code>list</code>：下钻到具体函数和代码行</h2><h3 id="11-1-基本用法">11.1 基本用法</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(pprof) list process</span><br></pre></td></tr></table></figure><p>它会把某个函数展开到源码级别，告诉你热点主要集中在哪几行。</p><p>示意输出大致像这样：</p><figure class="highlight text"><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">ROUTINE ======================== main.process</span><br><span class="line">     1.20s      1.40s (flat, cum) 46.67% of Total</span><br><span class="line">         .          .     10: func process(data []string) string &#123;</span><br><span class="line">     0.10s      0.10s     11:     var b strings.Builder</span><br><span class="line">     0.80s      0.90s     12:     for _, s := range data &#123;</span><br><span class="line">     0.20s      0.30s     13:         b.WriteString(strings.ToUpper(s))</span><br><span class="line">     0.10s      0.10s     14:     &#125;</span><br><span class="line">         .      0.10s     15:     return b.String()</span><br></pre></td></tr></table></figure><h3 id="11-2-为什么-list-很有价值">11.2 为什么 <code>list</code> 很有价值</h3><p>因为 <code>top</code> 只能告诉你“哪个函数热”，但一个函数里可能有多段逻辑。</p><p>而 <code>list</code> 能帮你继续往下缩小范围：</p><ul><li>是循环本身热</li><li>还是字符串转换热</li><li>还是内部分配热</li></ul><p>这就更接近真正能动手优化的位置。</p><hr><h2 id="12-web：看调用图">12. <code>web</code>：看调用图</h2><h3 id="12-1-作用">12.1 作用</h3><p><code>web</code> 会生成调用图，帮你从图形上看：</p><ul><li>谁调用了谁</li><li>哪条路径最粗</li><li>热点是“单点”还是“整条链路”</li></ul><p>在官方博客示例中，<code>web</code> 常用来辅助理解大块热点的来源。</p><h3 id="12-2-它适合什么情况">12.2 它适合什么情况</h3><p>当你面对：</p><ul><li>调用层级较深</li><li>很多函数彼此相调</li><li>只看 <code>top</code> 还不够直观</li></ul><p>时，图形化通常会更容易建立整体感。</p><h3 id="12-3-一个现实提醒">12.3 一个现实提醒</h3><p>有时调用图会很乱，所以它更适合作为：</p><ul><li>辅助理解全局结构</li></ul><p>而不是替代 <code>top</code> 和 <code>list</code>。</p><p>实战里经常是：</p><ol><li>先 <code>top</code></li><li>再 <code>list</code></li><li>必要时用 <code>web</code> 看整体路径</li></ol><hr><h2 id="13-CPU-Profile-应该怎么读">13. CPU Profile 应该怎么读</h2><h3 id="13-1-先找大头，不要先盯小百分比">13.1 先找大头，不要先盯小百分比</h3><p>假设 <code>top</code> 里你看到：</p><figure class="highlight text"><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">main.parseJSON          35%</span><br><span class="line">runtime.mallocgc        22%</span><br><span class="line">encoding/json.Unmarshal 18%</span><br><span class="line">bytes.(*Buffer).Write   5%</span><br></pre></td></tr></table></figure><p>此时优先级通常是：</p><ol><li>先看 <code>main.parseJSON</code></li><li>再看 <code>runtime.mallocgc</code></li><li>再看 <code>encoding/json.Unmarshal</code></li></ol><p>而不是一开始就去优化 5% 的点。</p><h3 id="13-2-如果-runtime-mallocgc-很高，说明什么">13.2 如果 <code>runtime.mallocgc</code> 很高，说明什么</h3><p>通常说明：</p><ul><li>分配比较多</li><li>GC 压力可能也大</li><li>CPU 时间有一部分花在“分配和回收”上</li></ul><p>这时你应该联想到：</p><ul><li>看内存 Profile</li><li>看 <code>allocs/op</code></li><li>看是不是创建了大量短命对象</li></ul><h3 id="13-3-如果热点在你自己的函数里，下一步怎么做">13.3 如果热点在你自己的函数里，下一步怎么做</h3><p>要继续看：</p><ul><li>这个函数是不是做了太多工作</li><li>是否存在重复计算</li><li>是否可以减少分配</li><li>是否可以换更合适的数据结构</li></ul><p>换句话说，<code>pprof</code> 负责告诉你“哪块最值得看”，但优化方案仍然要靠你结合代码理解来定。</p><hr><h2 id="14-内存-Profile-应该怎么读">14. 内存 Profile 应该怎么读</h2><h3 id="14-1-它主要回答两个问题">14.1 它主要回答两个问题</h3><ol><li>谁在分配大量内存</li><li>哪些分配最后仍然留在堆上</li></ol><h3 id="14-2-两种很常见的视角">14.2 两种很常见的视角</h3><p>虽然不同命令和视角很多，但入门阶段你先建立两个意识就够了：</p><ul><li><strong>看在用的内存</strong></li><li><strong>看总分配量</strong></li></ul><p>你可以把它们粗略理解为：</p><ul><li>“现在堆里还压着多少”</li><li>“运行过程中一共分出去过多少”</li></ul><h3 id="14-3-为什么这两个视角都重要">14.3 为什么这两个视角都重要</h3><p>有些问题是：</p><ul><li>总分配量巨大，但对象很快就死掉了</li></ul><p>这会带来 GC 压力。</p><p>有些问题是：</p><ul><li>某些对象长期存活，占着很多堆空间</li></ul><p>这会带来内存占用高的问题。</p><h3 id="14-4-一个典型例子">14.4 一个典型例子</h3><p>如果你看到某个函数：</p><ul><li><code>allocs/op</code> 很高</li><li><code>runtime.mallocgc</code> 很热</li><li>mem profile 里它的分配量也很大</li></ul><p>那通常说明它是重点优化对象。</p><hr><h2 id="15-一个完整例子：字符串拼接为什么慢">15. 一个完整例子：字符串拼接为什么慢</h2><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="function"><span class="keyword">func</span> <span class="title">ConcatPlus</span><span class="params">(strs []<span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    result := <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> strs &#123;</span><br><span class="line">        result += s</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你前面用 Benchmark 已经看到它可能很慢。</p><h3 id="15-1-现在加上-Profile">15.1 现在加上 Profile</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench BenchmarkConcatPlus -run ^$ -cpuprofile cpu.prof -memprofile mem.prof</span><br></pre></td></tr></table></figure><h3 id="15-2-你可能会看到什么">15.2 你可能会看到什么</h3><p>CPU 侧可能出现：</p><ul><li>字符串拼接相关函数</li><li><code>runtime.mallocgc</code></li></ul><p>内存侧可能看到：</p><ul><li>大量字符串分配</li><li>大量拷贝带来的分配热点</li></ul><h3 id="15-3-这时优化方向就更明确了">15.3 这时优化方向就更明确了</h3><p>你就知道：</p><ul><li>问题不只是“慢”</li><li>而是“频繁创建新字符串，导致大量分配和拷贝”</li></ul><p>于是改成：</p><ul><li><code>strings.Builder</code></li><li>或者 <code>strings.Join</code></li></ul><p>才是有依据的，而不是凭经验拍脑袋。</p><p>这就是 <code>pprof</code> 的价值。</p><hr><h2 id="16-调优顺序应该是什么">16. 调优顺序应该是什么</h2><p>很多人学工具时只记命令，不记流程。真正重要的是流程。</p><h3 id="16-1-推荐顺序">16.1 推荐顺序</h3><ol><li>先复现问题</li><li>用 Benchmark 或真实流量确认慢在哪里</li><li>采集 Profile</li><li>用 <code>top</code> 找热点</li><li>用 <code>list</code> / 调用图下钻</li><li>做最小必要改动</li><li>重新跑 Benchmark 和 Profile 验证收益</li></ol><h3 id="16-2-为什么要“最小必要改动”">16.2 为什么要“最小必要改动”</h3><p>因为性能问题最容易把人带进“顺手大改一堆”的陷阱。</p><p>但这样做的问题是：</p><ul><li>你不知道哪一处改动真正有效</li><li>容易引入行为回归</li><li>可维护性可能明显下降</li></ul><p>所以比较稳妥的做法通常是：</p><ul><li>一次只针对一个主要热点做优化</li><li>每次优化后立刻验证</li></ul><hr><h2 id="17-一个典型错误：拿-CPU-Profile-解决内存问题">17. 一个典型错误：拿 CPU Profile 解决内存问题</h2><p>工具不是万能的，不同问题要看对的视角。</p><h3 id="17-1-如果问题是“CPU-高”">17.1 如果问题是“CPU 高”</h3><p>优先：</p><ul><li>CPU Profile</li><li>Benchmark</li></ul><h3 id="17-2-如果问题是“内存涨得快”">17.2 如果问题是“内存涨得快”</h3><p>优先：</p><ul><li>Heap / Memory Profile</li><li><code>-benchmem</code></li><li>逃逸分析</li></ul><h3 id="17-3-如果问题是“锁竞争严重”">17.3 如果问题是“锁竞争严重”</h3><p>优先：</p><ul><li>Mutex Profile</li><li>Block Profile</li></ul><h3 id="17-4-这背后的原则">17.4 这背后的原则</h3><p><strong>先确认瓶颈类型，再选工具。</strong></p><p>不要把所有问题都拿一把锤子砸。</p><hr><h2 id="18-常见坑总结">18. 常见坑总结</h2><h3 id="18-1-没有稳定复现就开始采样">18.1 没有稳定复现就开始采样</h3><p>如果输入数据不稳定、路径不稳定，Profile 也会漂。</p><p>要尽量保证：</p><ul><li>输入固定</li><li>场景可重复</li><li>干扰尽量少</li></ul><h3 id="18-2-看到热点就直接重写">18.2 看到热点就直接重写</h3><p>热点只是说明“值得看”，不等于“必须推倒重来”。</p><p>很多时候：</p><ul><li>调整数据结构</li><li>减少分配</li><li>把某个操作移出循环</li></ul><p>就够了。</p><h3 id="18-3-只看一个维度">18.3 只看一个维度</h3><p>例如：</p><ul><li>只看 CPU，不看内存</li><li>只看 <code>ns/op</code>，不看 <code>allocs/op</code></li></ul><p>这很容易漏掉真正原因。</p><h3 id="18-4-优化后不回归验证">18.4 优化后不回归验证</h3><p>真正可靠的调优闭环一定要包括：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">定位 -&gt; 修改 -&gt; 复测</span><br></pre></td></tr></table></figure><p>否则你只是“感觉自己优化过了”。</p><h3 id="18-5-在非热点路径过度优化">18.5 在非热点路径过度优化</h3><p>如果某个函数只占 1% 总耗时，你花两小时优化它，整体收益可能几乎没有。</p><p>优先处理大头，这是性能分析最重要的收益之一。</p><h3 id="18-6-把-pprof-当成唯一真相">18.6 把 <code>pprof</code> 当成唯一真相</h3><p><code>pprof</code> 很强，但它依然只是工具。</p><p>你还要结合：</p><ul><li>代码上下文</li><li>Benchmark</li><li>真实业务场景</li><li>算法复杂度</li></ul><p>一起判断。</p><hr><h2 id="19-本课练习">19. 本课练习</h2><h3 id="练习-1：给一个-Benchmark-采集-CPU-Profile">练习 1：给一个 Benchmark 采集 CPU Profile</h3><p>任选你前面写过的一个 Benchmark，例如字符串拼接、切片构建或 map 查找：</p><ul><li>运行 <code>-cpuprofile</code></li><li>用 <code>go tool pprof</code> 打开</li><li>执行 <code>top</code></li></ul><p>写下你看到的前 3 个热点函数。</p><hr><h3 id="练习-2：采集内存-Profile">练习 2：采集内存 Profile</h3><p>对同一个 Benchmark 再运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -run ^$ -memprofile mem.prof</span><br></pre></td></tr></table></figure><p>观察：</p><ul><li>哪些函数分配更明显</li><li>是否出现了 <code>runtime.mallocgc</code> 等相关热点</li></ul><hr><h3 id="练习-3：用-list-下钻">练习 3：用 <code>list</code> 下钻</h3><p>找一个热点函数，执行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">list 函数名</span><br></pre></td></tr></table></figure><p>尝试回答：</p><ul><li>热点集中在哪几行</li><li>是循环、分配、字符串处理，还是容器操作导致的</li></ul><hr><h3 id="练习-4：做一次最小优化并回归验证">练习 4：做一次最小优化并回归验证</h3><p>例如：</p><ul><li>把 <code>+</code> 拼接改成 <code>strings.Builder</code></li><li>给切片预分配容量</li><li>给 map 预估容量</li></ul><p>然后重新跑：</p><ul><li>Benchmark</li><li>CPU Profile 或内存 Profile</li></ul><p>比较前后差异。</p><hr><h3 id="练习-5：为一个-HTTP-服务接入-pprof">练习 5：为一个 HTTP 服务接入 <code>pprof</code></h3><p>写一个最简单的 HTTP 服务，接入：</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="keyword">import</span> _ <span class="string">&quot;net/http/pprof&quot;</span></span><br></pre></td></tr></table></figure><p>并启动本地监听，然后尝试访问：</p><ul><li><code>/debug/pprof/</code></li><li><code>/debug/pprof/heap</code></li><li><code>/debug/pprof/profile</code></li></ul><p>理解这些端点各自的作用。</p><hr><h2 id="20-自测题">20. 自测题</h2><h3 id="20-1-概念题">20.1 概念题</h3><ol><li><code>pprof</code> 和 Benchmark 的核心区别是什么？</li><li>为什么性能优化最忌讳“凭感觉改代码”？</li><li>CPU Profile 和内存 Profile 分别更适合回答什么问题？</li><li>为什么说 <code>pprof</code> 很多时候采用的是采样思路？</li><li><code>top</code> 里的 <code>flat</code> 和 <code>cum</code> 分别表示什么？</li><li>为什么 <code>list</code> 比只看 <code>top</code> 更接近真实可优化位置？</li><li>为什么优化之后一定要重新跑 Benchmark 和 Profile？</li><li>为什么说性能工具要和代码理解结合起来使用？</li></ol><h3 id="20-2-代码阅读题">20.2 代码阅读题</h3><p>下面这个场景里，你应该优先看哪类 Profile？为什么？</p><figure class="highlight text"><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">一个字符串处理函数在 Benchmark 中显示 allocs/op 很高，</span><br><span class="line">而 CPU Profile 里 runtime.mallocgc 也占了比较明显的比例。</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>这个场景应该优先把注意力放在<strong>内存分配和分配来源</strong>上，因此要重点看：</p><ul><li>内存 Profile</li><li><code>-benchmem</code></li><li>必要时结合 CPU Profile</li></ul><p>原因是：</p><ol><li><code>allocs/op</code> 很高，说明每次操作都在做大量分配</li><li><code>runtime.mallocgc</code> 在 CPU Profile 中明显，说明 CPU 时间有一部分花在分配上</li><li>这类问题通常不是“纯计算慢”，而是“分配太多导致整体变慢”</li></ol><p>因此更合理的优化方向通常是：</p><ul><li>减少中间对象创建</li><li>减少字符串拷贝</li><li>预分配容量</li><li>改善数据结构或拼接方式</li></ul></details><hr><h2 id="21-本课总结">21. 本课总结</h2><p>这一课真正重要的，不是背命令，而是建立一套可靠的性能定位流程。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td><code>pprof</code> 目标</td><td>告诉你时间和内存主要花在哪里</td></tr><tr><td>CPU Profile</td><td>看时间热点</td></tr><tr><td>内存 Profile</td><td>看分配热点和堆占用</td></tr><tr><td>核心查看方式</td><td><code>top</code> 看大头，<code>list</code> 下钻，必要时看调用图</td></tr><tr><td>调优闭环</td><td>复现 -&gt; 采样 -&gt; 定位 -&gt; 修改 -&gt; 回归验证</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>不要凭感觉优化，先让数据告诉你瓶颈在哪</strong></li><li><strong>CPU 热点和内存热点常常相关，但不是同一个维度</strong></li><li><strong><code>top</code> 负责找方向，<code>list</code> 负责找位置</strong></li><li><strong>真正有效的优化，必须在修改后重新验证</strong></li></ol><hr><h2 id="22-下一课预告">22. 下一课预告</h2><p>你已经会用工具去找热点了。下一步，我们再把视角拉回代码本身，练一种更高级但也更长期受益的能力：读优秀源码。</p><p>下一课：<strong>阅读标准库与源码思维训练</strong></p><p>会重点讲：</p><ul><li>为什么阅读标准库是进阶 Go 的关键动作</li><li>读源码时应该先看什么，再看什么</li><li>怎么从接口设计、错误处理、命名和分层中学工程思路</li><li>遇到看不懂的源码时怎么拆解</li><li>如何把“读懂源码”变成“迁移到自己项目里的能力”</li></ul><p>学完下一课，你就会开始从“学语法”进入“学工程思维”的阶段。</p>]]></content>
    
    
    <summary type="html">Go语言系列38</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 37 课：深入理解切片、map 与接口底层</title>
    <link href="https://yjyrichard.github.io/posts/7c187e45.html"/>
    <id>https://yjyrichard.github.io/posts/7c187e45.html</id>
    <published>2026-03-22T15:28:11.768Z</published>
    <updated>2026-03-22T15:40:40.042Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 37 课：深入理解切片、map 与接口底层</h1><blockquote><p>学习定位：这是整套 Go 教程的第 37 课，也是阶段六（高级进阶阶段）的第四课。<br>前置要求：已经完成第 36 课，理解 Go 的切片、map、接口基本语法，以及内存与逃逸分析的基础认知。<br>本课目标：建立对切片、map 和接口底层表示的稳定理解，掌握切片头部结构、共享底层数组、扩容行为、map 的高层运行特征、接口值的“动态类型 + 动态值”模型，并能用这些认知解释常见 Bug、性能现象和语言限制。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面你已经会用：</p><ul><li><code>slice</code></li><li><code>map</code></li><li><code>interface</code></li></ul><p>但 Go 里很多“奇怪现象”，其实都和它们的底层表示有关。</p><p>例如：</p><ul><li>为什么切片传进函数后，改元素会影响外面？</li><li>为什么 <code>append</code> 之后，有时会影响原切片，有时不会？</li><li>为什么从大切片里截出一个很小的子切片，也可能迟迟不释放内存？</li><li>为什么 <code>nil map</code> 可以读，不能写？</li><li>为什么 <code>map</code> 元素不能直接取地址？</li><li>为什么接口明明“看起来是 nil”，结果 <code>err != nil</code>？</li></ul><p>如果你只停留在语法层，很多问题只能靠记结论。</p><p>而这一课的目标是：</p><p><strong>把这些现象背后的结构看清楚。</strong></p><p>这样你以后遇到问题时，能自己推导，而不是死记。</p><hr><h2 id="2-先说明一个边界：我们学的是“稳定理解”，不是死背-runtime-细节">2. 先说明一个边界：我们学的是“稳定理解”，不是死背 runtime 细节</h2><p>Go 的运行时实现会随版本演进，一些非常细的内部布局可能调整。</p><p>所以这节课的重点不是：</p><ul><li>背某个版本具体桶布局细节</li><li>背某个版本切片扩容倍率的每一个分支</li></ul><p>而是掌握那些<strong>长期稳定、能解释现象、能指导代码设计</strong>的认知：</p><ul><li>切片本质上不是数组本身，而是一个“视图”</li><li>map 不是简单数组，而是运行时管理的哈希结构</li><li>接口值本质上同时带着“动态类型”和“动态值”</li></ul><p>你把这三点吃透，后面的很多问题都会顺很多。</p><hr><h2 id="3-切片到底是什么：它不是数组本身">3. 切片到底是什么：它不是数组本身</h2><p>很多人学切片时，表面会用：</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">s := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br></pre></td></tr></table></figure><p>但心里容易误以为：</p><blockquote><p>切片就是这三个元素本身。</p></blockquote><p>其实更准确的理解是：</p><blockquote><p>切片是一个描述符，它描述了一段底层数组中的连续区域。</p></blockquote><p>也就是说，切片本身通常可以理解为包含三部分信息：</p><ol><li>指向底层数组某个位置的指针</li><li>长度 <code>len</code></li><li>容量 <code>cap</code></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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> slice <span class="keyword">struct</span> &#123;</span><br><span class="line">    ptr *T</span><br><span class="line">    <span class="built_in">len</span> <span class="type">int</span></span><br><span class="line">    <span class="built_in">cap</span> <span class="type">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意：</p><ul><li>这只是帮助理解的伪结构</li><li>不是你在业务里实际写的代码</li></ul><h3 id="3-1-这意味着什么">3.1 这意味着什么</h3><p>意味着切片值本身很小，复制一个切片，通常只是复制这三项元信息，而不是把底层所有元素复制一遍。</p><p>这也是为什么：</p><ul><li>切片传参看起来像“传值”</li><li>但改元素又会影响外部</li></ul><p>因为复制的是“切片头”，不是“底层数组数据”。</p><hr><h2 id="4-数组和切片的核心差别">4. 数组和切片的核心差别</h2><h3 id="4-1-数组是值本身">4.1 数组是值本身</h3><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">arr := [<span class="number">3</span>]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br></pre></td></tr></table></figure><p>数组包含的就是那 3 个元素本身。</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></pre></td><td class="code"><pre><span class="line">a := [<span class="number">3</span>]<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">b := a</span><br><span class="line">b[<span class="number">0</span>] = <span class="number">100</span></span><br><span class="line"></span><br><span class="line">fmt.Println(a) <span class="comment">// [1 2 3]</span></span><br><span class="line">fmt.Println(b) <span class="comment">// [100 2 3]</span></span><br></pre></td></tr></table></figure><h3 id="4-2-切片是对数组的一层包装">4.2 切片是对数组的一层包装</h3><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">s1 := []<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">s2 := s1</span><br><span class="line">s2[<span class="number">0</span>] = <span class="number">100</span></span><br><span class="line"></span><br><span class="line">fmt.Println(s1) <span class="comment">// [100 2 3]</span></span><br><span class="line">fmt.Println(s2) <span class="comment">// [100 2 3]</span></span><br></pre></td></tr></table></figure><p>为什么会这样？</p><p>因为：</p><ul><li><code>s1</code> 和 <code>s2</code> 的切片头是两份</li><li>但它们指向同一块底层数组</li></ul><h3 id="4-3-一个非常实用的结论">4.3 一个非常实用的结论</h3><p><strong>数组偏“真正数据本体”，切片偏“对数据的一层视图”。</strong></p><p>你带着这个理解，后面很多现象都会自然很多。</p><hr><h2 id="5-len-和-cap-到底分别表示什么">5. <code>len</code> 和 <code>cap</code> 到底分别表示什么</h2><h3 id="5-1-len">5.1 <code>len</code></h3><p><code>len</code> 表示当前切片可直接访问的元素个数。</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">s := []<span class="type">int</span>&#123;<span class="number">10</span>, <span class="number">20</span>, <span class="number">30</span>&#125;</span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s)) <span class="comment">// 3</span></span><br></pre></td></tr></table></figure><h3 id="5-2-cap">5.2 <code>cap</code></h3><p><code>cap</code> 表示从当前切片起点开始，到底层数组末尾还能容纳多少元素。</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></pre></td><td class="code"><pre><span class="line">arr := [<span class="number">5</span>]<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>&#125;</span><br><span class="line">s := arr[<span class="number">1</span>:<span class="number">3</span>]</span><br><span class="line"></span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s)) <span class="comment">// 2</span></span><br><span class="line">fmt.Println(<span class="built_in">cap</span>(s)) <span class="comment">// 4</span></span><br></pre></td></tr></table></figure><p>为什么容量是 4？</p><p>因为 <code>s</code> 从 <code>arr[1]</code> 开始，到数组结尾一共有：</p><ul><li><code>arr[1]</code></li><li><code>arr[2]</code></li><li><code>arr[3]</code></li><li><code>arr[4]</code></li></ul><p>共 4 个位置。</p><h3 id="5-3-为什么-cap-很重要">5.3 为什么 <code>cap</code> 很重要</h3><p>因为 <code>append</code> 是否需要扩容，跟 <code>cap</code> 直接相关。</p><ul><li>如果还有容量，可能直接往原底层数组后面写</li><li>如果没容量，就要分配新数组并拷贝数据</li></ul><hr><h2 id="6-切片为什么会“共享底层数组”">6. 切片为什么会“共享底层数组”</h2><p>这是切片最重要的行为特征之一。</p><h3 id="6-1-例子">6.1 例子</h3><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></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">    arr := [<span class="number">5</span>]<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>&#125;</span><br><span class="line"></span><br><span class="line">    s1 := arr[<span class="number">1</span>:<span class="number">4</span>] <span class="comment">// [2 3 4]</span></span><br><span class="line">    s2 := arr[<span class="number">2</span>:<span class="number">5</span>] <span class="comment">// [3 4 5]</span></span><br><span class="line"></span><br><span class="line">    s1[<span class="number">1</span>] = <span class="number">999</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(arr) <span class="comment">// [1 2 999 4 5]</span></span><br><span class="line">    fmt.Println(s1)  <span class="comment">// [2 999 4]</span></span><br><span class="line">    fmt.Println(s2)  <span class="comment">// [999 4 5]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-为什么-s2-也变了">6.2 为什么 <code>s2</code> 也变了</h3><p>因为：</p><ul><li><code>s1[1]</code> 对应的是底层数组里的某个位置</li><li><code>s2[0]</code> 恰好也映射到同一个位置</li></ul><p>你改的是底层数组，不只是某个“切片副本”。</p><h3 id="6-3-这带来的两个后果">6.3 这带来的两个后果</h3><ol><li>切片操作很高效，因为不需要总是复制数据</li><li>也很容易出现“看起来没关系，其实互相影响”的 Bug</li></ol><hr><h2 id="7-append-为什么有时影响原切片，有时不影响">7. <code>append</code> 为什么有时影响原切片，有时不影响</h2><p>这是 Go 切片里最经典、也最容易把人绕晕的问题。</p><h3 id="7-1-先看第一种情况：容量够，直接复用原数组">7.1 先看第一种情况：容量够，直接复用原数组</h3><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 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">    s1 := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">2</span>, <span class="number">4</span>)</span><br><span class="line">    s1[<span class="number">0</span>] = <span class="number">1</span></span><br><span class="line">    s1[<span class="number">1</span>] = <span class="number">2</span></span><br><span class="line"></span><br><span class="line">    s2 := s1</span><br><span class="line">    s2 = <span class="built_in">append</span>(s2, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Println(s1) <span class="comment">// [1 2]</span></span><br><span class="line">    fmt.Println(s2) <span class="comment">// [1 2 3]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>表面看 <code>s1</code> 没变，但底层数组其实已经被共享更新了。只是 <code>s1</code> 的长度还是 2，所以你直接打印不到第 3 个元素。</p><p>如果再切出来看：</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">fmt.Println(s1[:<span class="number">3</span>]) <span class="comment">// [1 2 3]</span></span><br></pre></td></tr></table></figure><h3 id="7-2-第二种情况：容量不够，触发扩容">7.2 第二种情况：容量不够，触发扩容</h3><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></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">    s1 := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line">    s2 := s1</span><br><span class="line"></span><br><span class="line">    s2 = <span class="built_in">append</span>(s2, <span class="number">3</span>)</span><br><span class="line">    s2[<span class="number">0</span>] = <span class="number">100</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(s1) <span class="comment">// 可能仍是 [1 2]</span></span><br><span class="line">    fmt.Println(s2) <span class="comment">// [100 2 3]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当 <code>append</code> 导致新分配底层数组后：</p><ul><li><code>s2</code> 指向新数组</li><li><code>s1</code> 仍指向旧数组</li></ul><p>于是它们就“分家”了。</p><h3 id="7-3-实用结论">7.3 实用结论</h3><p><code>append</code> 之后到底还共享不共享底层数组，关键看：</p><ul><li>原切片是否还有剩余容量</li><li>是否触发了重新分配</li></ul><p>这也是为什么切片相关 Bug 经常很隐蔽。</p><hr><h2 id="8-切片传参为什么能改到外面">8. 切片传参为什么能改到外面</h2><h3 id="8-1-例子">8.1 例子</h3><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"><span class="function"><span class="keyword">func</span> <span class="title">UpdateFirst</span><span class="params">(s []<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    s[<span class="number">0</span>] = <span class="number">999</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">    nums := []<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">    UpdateFirst(nums)</span><br><span class="line">    fmt.Println(nums) <span class="comment">// [999 2 3]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-2-这是“传值”还是“传引用”">8.2 这是“传值”还是“传引用”</h3><p>严格说，Go 里依然是<strong>传值</strong>。</p><p>传进去的是切片头的一份副本。</p><p>但因为这份副本里的 <code>ptr</code> 仍然指向同一块底层数组，所以改元素会影响外部。</p><h3 id="8-3-但是直接改切片长度不会反映到外面">8.3 但是直接改切片长度不会反映到外面</h3><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"><span class="function"><span class="keyword">func</span> <span class="title">Grow</span><span class="params">(s []<span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    s = <span class="built_in">append</span>(s, <span class="number">100</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">    nums := []<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">    Grow(nums)</span><br><span class="line">    fmt.Println(nums) <span class="comment">// 仍然是 [1 2 3]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么？</p><p>因为：</p><ul><li>函数里修改的是切片头副本</li><li>外部那个切片头的 <code>len</code> 没变</li></ul><p>所以一个很关键的区别是：</p><ul><li>改底层元素，外面可能看见</li><li>改切片头本身，外面看不见，除非你返回新切片或传 <code>*[]T</code></li></ul><hr><h2 id="9-子切片可能导致“大对象迟迟不释放”">9. 子切片可能导致“大对象迟迟不释放”</h2><p>这是切片在工程里非常实用、也非常容易忽略的一个点。</p><h3 id="9-1-例子">9.1 例子</h3><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="function"><span class="keyword">func</span> <span class="title">First100</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">byte</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> data[:<span class="number">100</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>假设 <code>data</code> 是一个 10MB 的大切片。</p><p>你只想要前 100 个字节，看起来返回 <code>data[:100]</code> 很合理。</p><p>但问题是：</p><ul><li>这个小切片仍然指向那 10MB 底层数组</li><li>只要这 100 字节切片还活着</li><li>那整块底层数组就可能不能被回收</li></ul><h3 id="9-2-这就是“切片内存滞留”问题">9.2 这就是“切片内存滞留”问题</h3><p>你逻辑上只保留了一小部分数据，但物理上仍然拖着整块大数组。</p><h3 id="9-3-正确做法">9.3 正确做法</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">First100Copy</span><span class="params">(data []<span class="type">byte</span>)</span></span> []<span class="type">byte</span> &#123;</span><br><span class="line">    result := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">100</span>)</span><br><span class="line">    <span class="built_in">copy</span>(result, data[:<span class="number">100</span>])</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样新切片就不再引用原大数组。</p><h3 id="9-4-一个经验原则">9.4 一个经验原则</h3><p><strong>短期处理子切片没问题，长期持有小片段时要警惕背后是否绑着大数组。</strong></p><hr><h2 id="10-复制切片，到底复制了什么">10. 复制切片，到底复制了什么</h2><h3 id="10-1-直接赋值">10.1 直接赋值</h3><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">s1 := []<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">s2 := s1</span><br></pre></td></tr></table></figure><p>这只是复制了切片头，底层数组共享。</p><h3 id="10-2-真正复制数据">10.2 真正复制数据</h3><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">s1 := []<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">s2 := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="built_in">len</span>(s1))</span><br><span class="line"><span class="built_in">copy</span>(s2, s1)</span><br></pre></td></tr></table></figure><p>这样才是两份独立底层数据。</p><h3 id="10-3-一个实用判断">10.3 一个实用判断</h3><p>如果你需要：</p><ul><li>避免互相影响</li><li>保证后续修改隔离</li><li>避免长期引用大底层数组</li></ul><p>那就应该显式复制，而不是只做赋值。</p><hr><h2 id="11-map-的本质：运行时管理的哈希结构">11. map 的本质：运行时管理的哈希结构</h2><p>接下来讲 map。</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">m := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">    <span class="string">&quot;a&quot;</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="string">&quot;b&quot;</span>: <span class="number">2</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>表面上它像一个“键值容器”，但更底层一点地看：</p><blockquote><p>map 是运行时维护的一种哈希表结构，负责根据 key 快速定位 value。</p></blockquote><p>这意味着：</p><ul><li>key 不是按插入顺序排的</li><li>底层可能会重排、搬迁、扩容</li><li>元素位置对程序员来说不是稳定地址</li></ul><p>这几个点会直接解释很多语言限制。</p><hr><h2 id="12-为什么-map-的-key-必须可比较">12. 为什么 map 的 key 必须可比较</h2><h3 id="12-1-语言规则">12.1 语言规则</h3><p>map 的 key 必须是可比较类型。</p><p>比如这些可以：</p><ul><li><code>int</code></li><li><code>string</code></li><li><code>bool</code></li><li>指针</li><li>可比较结构体</li></ul><p>这些不行：</p><ul><li><code>slice</code></li><li><code>map</code></li><li><code>func</code></li></ul><h3 id="12-2-为什么">12.2 为什么</h3><p>因为 map 需要判断：</p><ul><li>两个 key 是否相等</li><li>某个 key 应该放在哪个哈希位置</li></ul><p>如果一个类型连 <code>==</code> 都不支持，就没法作为稳定键使用。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 编译错误</span></span><br><span class="line"><span class="keyword">var</span> m <span class="keyword">map</span>[[]<span class="type">int</span>]<span class="type">string</span></span><br></pre></td></tr></table></figure><h3 id="12-3-和泛型里的-comparable-是相通的">12.3 和泛型里的 <code>comparable</code> 是相通的</h3><p>你前面学过：</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="keyword">type</span> Set[T comparable] <span class="keyword">map</span>[T]<span class="keyword">struct</span>&#123;&#125;</span><br></pre></td></tr></table></figure><p>这和 map key 的要求本质一致。</p><hr><h2 id="13-nil-map-为什么可以读，不能写">13. <code>nil map</code> 为什么可以读，不能写</h2><h3 id="13-1-例子">13.1 例子</h3><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="keyword">var</span> m <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span></span><br><span class="line"></span><br><span class="line">fmt.Println(m[<span class="string">&quot;a&quot;</span>]) <span class="comment">// 0</span></span><br><span class="line">fmt.Println(<span class="built_in">len</span>(m)) <span class="comment">// 0</span></span><br><span class="line"></span><br><span class="line">m[<span class="string">&quot;a&quot;</span>] = <span class="number">1</span> <span class="comment">// panic</span></span><br></pre></td></tr></table></figure><h3 id="13-2-为什么读可以">13.2 为什么读可以</h3><p>因为对 <code>nil map</code> 的读取，Go 设计成了“返回零值”。</p><p>这样做有一定便利性，很多只读逻辑不需要先判空。</p><h3 id="13-3-为什么写不行">13.3 为什么写不行</h3><p>因为 <code>nil map</code> 还没有真正初始化底层存储结构。</p><p>你必须先：</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">m = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br></pre></td></tr></table></figure><p>之后才能写。</p><h3 id="13-4-一个类比理解">13.4 一个类比理解</h3><p>可以把 <code>nil map</code> 想成：</p><ul><li>“一个合法存在但还没分配实际表结构的 map 句柄”</li></ul><p>它知道自己是 map，但没有可写入的数据容器。</p><hr><h2 id="14-为什么-map-元素不能直接取地址">14. 为什么 map 元素不能直接取地址</h2><p>这是 Go 里非常经典的一条限制。</p><h3 id="14-1-错误示例">14.1 错误示例</h3><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">m := <span class="keyword">map</span>[<span class="type">string</span>]User&#123;</span><br><span class="line">    <span class="string">&quot;u1&quot;</span>: &#123;Name: <span class="string">&quot;张三&quot;</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 编译错误</span></span><br><span class="line">p := &amp;m[<span class="string">&quot;u1&quot;</span>]</span><br></pre></td></tr></table></figure><h3 id="14-2-为什么语言不允许">14.2 为什么语言不允许</h3><p>因为 map 底层在扩容、迁移、重排时，元素所在位置可能变化。</p><p>也就是说：</p><ul><li>你今天看到的元素位置</li><li>过一会儿可能就不是那个位置了</li></ul><p>如果允许你长期持有元素地址，就会非常危险。</p><p>所以 Go 干脆在语言层禁止这件事。</p><h3 id="14-3-那要怎么改-map-中的结构体字段">14.3 那要怎么改 map 中的结构体字段</h3><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></pre></td><td class="code"><pre><span class="line">u := m[<span class="string">&quot;u1&quot;</span>]</span><br><span class="line">u.Name = <span class="string">&quot;李四&quot;</span></span><br><span class="line">m[<span class="string">&quot;u1&quot;</span>] = u</span><br></pre></td></tr></table></figure><p>第二种：map 里直接存指针。</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">m := <span class="keyword">map</span>[<span class="type">string</span>]*User&#123;</span><br><span class="line">    <span class="string">&quot;u1&quot;</span>: &#123;Name: <span class="string">&quot;张三&quot;</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">m[<span class="string">&quot;u1&quot;</span>].Name = <span class="string">&quot;李四&quot;</span></span><br></pre></td></tr></table></figure><h3 id="14-4-这两种方案怎么选">14.4 这两种方案怎么选</h3><ul><li>存值：更独立、更安全，但更新结构体字段要写回</li><li>存指针：修改方便，但共享状态更多，生命周期和 GC 成本也要考虑</li></ul><p>这和上一课“值 vs 指针”的判断是一脉相承的。</p><hr><h2 id="15-map-遍历顺序为什么不稳定">15. map 遍历顺序为什么不稳定</h2><h3 id="15-1-结论先说">15.1 结论先说</h3><p><strong>Go 不保证 map 的遍历顺序。</strong></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">m := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">    <span class="string">&quot;a&quot;</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="string">&quot;b&quot;</span>: <span class="number">2</span>,</span><br><span class="line">    <span class="string">&quot;c&quot;</span>: <span class="number">3</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> k, v := <span class="keyword">range</span> m &#123;</span><br><span class="line">    fmt.Println(k, v)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每次输出顺序都不应该被依赖。</p><h3 id="15-2-为什么不能依赖顺序">15.2 为什么不能依赖顺序</h3><p>因为 map 的目标是：</p><ul><li>高效查找</li><li>高效插入删除</li></ul><p>而不是保持插入顺序。</p><p>底层哈希结构决定了：</p><ul><li>元素组织方式不等同于“按插入排队”</li><li>运行时还可能因为扩容等操作改变内部布局</li></ul><h3 id="15-3-如果你需要稳定顺序怎么办">15.3 如果你需要稳定顺序怎么办</h3><p>就不要直接依赖 map 遍历顺序。</p><p>常见做法是：</p><ol><li>先把 key 收集到切片</li><li>对 key 排序</li><li>再按排序结果访问 map</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><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">keys := <span class="built_in">make</span>([]<span class="type">string</span>, <span class="number">0</span>, <span class="built_in">len</span>(m))</span><br><span class="line"><span class="keyword">for</span> k := <span class="keyword">range</span> m &#123;</span><br><span class="line">    keys = <span class="built_in">append</span>(keys, k)</span><br><span class="line">&#125;</span><br><span class="line">sort.Strings(keys)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, k := <span class="keyword">range</span> keys &#123;</span><br><span class="line">    fmt.Println(k, m[k])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="16-map-为什么并发不安全">16. map 为什么并发不安全</h2><h3 id="16-1-结论">16.1 结论</h3><p>普通 map 在并发读写场景下不安全。</p><p>尤其是：</p><ul><li>一个 goroutine 写</li><li>另一个 goroutine 同时读或写</li></ul><p>很容易出问题，甚至运行时报错。</p><h3 id="16-2-为什么">16.2 为什么</h3><p>因为 map 底层结构在写操作时可能发生变化：</p><ul><li>插入</li><li>删除</li><li>扩容</li><li>重排</li></ul><p>如果没有同步保护，另一个 goroutine 看到的状态可能不一致。</p><h3 id="16-3-常见解决方式">16.3 常见解决方式</h3><ul><li>外部加 <code>sync.Mutex</code> / <code>sync.RWMutex</code></li><li>用 <code>sync.Map</code> 处理特定模式</li></ul><p>注意：</p><p><code>sync.Map</code> 不是普通 map 的“全面替代品”，它适合某些特定并发场景，不适合盲目替换。</p><hr><h2 id="17-接口到底是什么：不是“方法列表”那么简单">17. 接口到底是什么：不是“方法列表”那么简单</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Reader <span class="keyword">interface</span> &#123;</span><br><span class="line">    Read(p []<span class="type">byte</span>) (<span class="type">int</span>, <span class="type">error</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但运行时里，一个接口值真正承载的，不只是“这个接口有个 Read 方法”。</p><p>更重要的理解是：</p><blockquote><p>一个接口值通常同时包含两部分信息：</p><ol><li>动态类型</li><li>动态值</li></ol></blockquote><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">type</span> iface <span class="keyword">struct</span> &#123;</span><br><span class="line">    typeInfo ...</span><br><span class="line">    data     ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样注意：</p><ul><li>这只是帮助理解的伪结构</li><li>不是业务代码里真实可写的类型</li></ul><hr><h2 id="18-什么叫“动态类型-动态值”">18. 什么叫“动态类型 + 动态值”</h2><h3 id="18-1-例子">18.1 例子</h3><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">var</span> x any</span><br><span class="line"> x = <span class="number">10</span></span><br></pre></td></tr></table></figure><p>这时接口值 <code>x</code> 里大致承载的是：</p><ul><li>动态类型：<code>int</code></li><li>动态值：<code>10</code></li></ul><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">var</span> x any</span><br><span class="line">x = <span class="string">&quot;hello&quot;</span></span><br></pre></td></tr></table></figure><p>这时就变成：</p><ul><li>动态类型：<code>string</code></li><li>动态值：<code>&quot;hello&quot;</code></li></ul><h3 id="18-2-为什么这很重要">18.2 为什么这很重要</h3><p>因为接口值是否相等、是否为 nil、做类型断言时发生什么，都和这两部分直接相关。</p><hr><h2 id="19-最经典的坑：接口里的-nil-不等于接口本身是-nil">19. 最经典的坑：接口里的 nil 不等于接口本身是 nil</h2><p>这是 Go 接口里最著名的坑之一。</p><h3 id="19-1-看代码">19.1 看代码</h3><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></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="keyword">type</span> MyError <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(e *MyError)</span></span> Error() <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;my error&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">ReturnErr</span><span class="params">()</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> e *MyError = <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">return</span> e</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">    err := ReturnErr()</span><br><span class="line">    fmt.Println(err == <span class="literal">nil</span>) <span class="comment">// false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="19-2-为什么结果是-false">19.2 为什么结果是 <code>false</code></h3><p>因为返回出来的 <code>err</code> 这个接口值里是：</p><ul><li>动态类型：<code>*MyError</code></li><li>动态值：<code>nil</code></li></ul><p>而一个真正的 nil 接口应该是：</p><ul><li>动态类型：<code>nil</code></li><li>动态值：<code>nil</code></li></ul><p>也就是说：</p><p><strong>接口值只要动态类型不为 nil，这个接口本身就不是 nil。</strong></p><h3 id="19-3-这就是很多-err-nil-诡异现象的根源">19.3 这就是很多 <code>err != nil</code> 诡异现象的根源</h3><p>如果你理解了“接口 = 动态类型 + 动态值”，这个坑就不诡异了。</p><hr><h2 id="20-接口赋值时会发生什么">20. 接口赋值时会发生什么</h2><h3 id="20-1-一个值放进接口，不只是语法转换">20.1 一个值放进接口，不只是语法转换</h3><p>例如：</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="keyword">var</span> x any = <span class="number">123</span></span><br></pre></td></tr></table></figure><p>你可以理解为：</p><ul><li>把 <code>123</code> 包装成一个接口值</li><li>同时附上它的真实类型信息</li></ul><p>这也是为什么接口如此灵活，但有时也会带来：</p><ul><li>额外间接层</li><li>动态分派</li><li>某些场景下的额外分配</li></ul><h3 id="20-2-这和上一课的逃逸分析有什么关系">20.2 这和上一课的逃逸分析有什么关系</h3><p>因为把值放进接口后，编译器要处理：</p><ul><li>动态类型信息</li><li>动态值表示</li></ul><p>在某些场景下，这会影响是否逃逸、是否分配。</p><p>所以接口虽然非常好用，但在高频热点路径里也要有性能意识。</p><hr><h2 id="21-类型断言和-type-switch，本质上在做什么">21. 类型断言和 type switch，本质上在做什么</h2><h3 id="21-1-类型断言">21.1 类型断言</h3><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> x any = <span class="number">123</span></span><br><span class="line"></span><br><span class="line">v, ok := x.(<span class="type">int</span>)</span><br><span class="line">fmt.Println(v, ok) <span class="comment">// 123 true</span></span><br></pre></td></tr></table></figure><p>本质上是在问：</p><blockquote><p>这个接口值里的动态类型，是不是 <code>int</code>？</p></blockquote><p>如果是，就把动态值取出来。</p><h3 id="21-2-type-switch">21.2 type switch</h3><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"><span class="keyword">switch</span> v := x.(<span class="keyword">type</span>) &#123;</span><br><span class="line"><span class="keyword">case</span> <span class="type">int</span>:</span><br><span class="line">    fmt.Println(<span class="string">&quot;int&quot;</span>, v)</span><br><span class="line"><span class="keyword">case</span> <span class="type">string</span>:</span><br><span class="line">    fmt.Println(<span class="string">&quot;string&quot;</span>, v)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">    fmt.Println(<span class="string">&quot;unknown&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这本质上也是对接口动态类型的分支判断。</p><h3 id="21-3-为什么这和接口底层理解有关">21.3 为什么这和接口底层理解有关</h3><p>因为你一旦知道接口里有“动态类型”，就明白类型断言并不神秘，它就是在检查这一部分信息。</p><hr><h2 id="22-这三类底层结构，如何影响日常代码">22. 这三类底层结构，如何影响日常代码</h2><p>现在把切片、map、接口串起来看。</p><h3 id="22-1-切片影响“共享与扩容”">22.1 切片影响“共享与扩容”</h3><p>你需要警惕：</p><ul><li>共享底层数组</li><li><code>append</code> 后是否分家</li><li>小切片引用大数组</li></ul><h3 id="22-2-map-影响“地址稳定性与顺序假设”">22.2 map 影响“地址稳定性与顺序假设”</h3><p>你需要警惕：</p><ul><li>不能依赖遍历顺序</li><li>不能直接取元素地址</li><li>并发读写不安全</li></ul><h3 id="22-3-接口影响“nil-判断与动态分派”">22.3 接口影响“nil 判断与动态分派”</h3><p>你需要警惕：</p><ul><li>带类型的 nil 接口</li><li>类型断言失败</li><li>高通用性带来的额外运行时成本</li></ul><h3 id="22-4-一个更高级的统一视角">22.4 一个更高级的统一视角</h3><p>这三者都说明了一件事：</p><blockquote><p>Go 表面语法虽然简单，但很多值背后其实不只是“原始数据”，而是一层运行时语义。</p></blockquote><p>理解这点后，你对语言的判断会明显更稳。</p><hr><h2 id="23-常见坑总结">23. 常见坑总结</h2><h3 id="23-1-以为切片赋值就是深拷贝">23.1 以为切片赋值就是深拷贝</h3><p>不是。</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">s2 := s1</span><br></pre></td></tr></table></figure><p>通常只是复制切片头，底层数组仍共享。</p><h3 id="23-2-忘了-append-可能改到共享底层数组">23.2 忘了 <code>append</code> 可能改到共享底层数组</h3><p>如果原切片还有容量，<code>append</code> 可能直接写入原数组后部。</p><p>这在多切片共享同一底层数组时很容易埋 Bug。</p><h3 id="23-3-小切片长期引用大数组">23.3 小切片长期引用大数组</h3><p>这是切片常见的内存滞留问题。</p><p>需要时要主动 <code>copy</code>。</p><h3 id="23-4-对-nil-map-直接写入">23.4 对 <code>nil map</code> 直接写入</h3><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">var</span> m <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span></span><br><span class="line">m[<span class="string">&quot;a&quot;</span>] = <span class="number">1</span> <span class="comment">// panic</span></span><br></pre></td></tr></table></figure><p>写入前必须 <code>make</code>。</p><h3 id="23-5-依赖-map-遍历顺序">23.5 依赖 map 遍历顺序</h3><p>任何依赖 map <code>range</code> 顺序的代码都是不稳的。</p><p>需要稳定顺序时，请显式排序。</p><h3 id="23-6-想直接改-map-string-User-里字段">23.6 想直接改 <code>map[string]User</code> 里字段</h3><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">m[<span class="string">&quot;u1&quot;</span>].Name = <span class="string">&quot;李四&quot;</span> <span class="comment">// 编译错误</span></span><br></pre></td></tr></table></figure><p>要么取出改完写回，要么 map 存指针。</p><h3 id="23-7-误判接口-nil">23.7 误判接口 nil</h3><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> e *MyError = <span class="literal">nil</span></span><br><span class="line"><span class="keyword">var</span> err <span class="type">error</span> = e</span><br><span class="line"></span><br><span class="line">fmt.Println(err == <span class="literal">nil</span>) <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>这个坑必须形成肌肉记忆。</p><h3 id="23-8-在高频路径里滥用接口和反射，却完全不测">23.8 在高频路径里滥用接口和反射，却完全不测</h3><p>这些抽象通常都值得用，但在热点路径上，要靠 Benchmark 验证，而不是拍脑袋。</p><hr><h2 id="24-本课练习">24. 本课练习</h2><h3 id="练习-1：观察切片共享底层数组">练习 1：观察切片共享底层数组</h3><p>写一个数组，基于它切两个重叠切片：</p><ul><li>修改第一个切片的元素</li><li>观察第二个切片和原数组的变化</li></ul><p>要求写出你自己的解释。</p><hr><h3 id="练习-2：观察-append-是否分配新数组">练习 2：观察 <code>append</code> 是否分配新数组</h3><p>准备两组切片：</p><ol><li>一组容量足够</li><li>一组容量刚好满</li></ol><p>分别做 <code>append</code>，观察：</p><ul><li>原切片是否还能看到新元素</li><li>修改追加后切片的元素是否影响原切片</li></ul><hr><h3 id="练习-3：修复小切片引用大数组">练习 3：修复小切片引用大数组</h3><p>模拟读取一个很大的 <code>[]byte</code>，只需要保留前 100 字节：</p><ul><li>先写直接切片版本</li><li>再写 <code>copy</code> 版本</li></ul><p>解释为什么第二种更适合长期保存。</p><hr><h3 id="练习-4：修改-map-中结构体值">练习 4：修改 map 中结构体值</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>分别尝试：</p><ol><li><code>map[string]User</code></li><li><code>map[string]*User</code></li></ol><p>完成字段更新，并比较两种方式的优缺点。</p><hr><h3 id="练习-5：复现接口-nil-坑">练习 5：复现接口 nil 坑</h3><p>自己定义一个实现 <code>error</code> 的类型，写一个函数返回“看起来是 nil 的错误”，然后验证：</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">err == <span class="literal">nil</span></span><br></pre></td></tr></table></figure><p>为什么结果和直觉不同。</p><hr><h2 id="25-自测题">25. 自测题</h2><h3 id="25-1-概念题">25.1 概念题</h3><ol><li>切片和数组最核心的底层差别是什么？</li><li>为什么切片赋值后，修改元素可能互相影响？</li><li><code>append</code> 之后两个切片是否仍共享底层数组，关键取决于什么？</li><li>为什么小子切片可能导致大数组迟迟不释放？</li><li>为什么 map 的 key 必须是可比较类型？</li><li>为什么 <code>nil map</code> 可以读但不能写？</li><li>为什么 Go 不允许直接取 map 元素地址？</li><li>接口值为什么说由“动态类型 + 动态值”组成？</li><li>为什么带类型的 nil 放进接口后，接口可能不等于 nil？</li><li>类型断言本质上是在检查什么？</li></ol><h3 id="25-2-代码阅读题">25.2 代码阅读题</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><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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    s1 := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">2</span>, <span class="number">4</span>)</span><br><span class="line">    s1[<span class="number">0</span>] = <span class="number">1</span></span><br><span class="line">    s1[<span class="number">1</span>] = <span class="number">2</span></span><br><span class="line"></span><br><span class="line">    s2 := s1</span><br><span class="line">    s2 = <span class="built_in">append</span>(s2, <span class="number">3</span>)</span><br><span class="line">    s2[<span class="number">0</span>] = <span class="number">100</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(s1)</span><br><span class="line">    fmt.Println(s2)</span><br><span class="line">    fmt.Println(s1[:<span class="number">3</span>])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>输出通常会是：</p><figure class="highlight text"><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">[100 2]</span><br><span class="line">[100 2 3]</span><br><span class="line">[100 2 3]</span><br></pre></td></tr></table></figure><p>原因是：</p><ol><li><code>s1</code> 初始长度为 2，容量为 4</li><li><code>s2 := s1</code> 后，两者共享同一底层数组</li><li><code>append(s2, 3)</code> 时，由于容量还够，没有重新分配新数组</li><li>所以追加的 <code>3</code> 实际写进了共享底层数组</li><li><code>s2[0] = 100</code> 也改的是同一底层数组，所以 <code>s1[0]</code> 也变成了 <code>100</code></li><li><code>s1</code> 的长度还是 2，所以直接打印只显示 <code>[100 2]</code></li><li>但 <code>s1[:3]</code> 可以看到底层数组里的第三个元素 <code>3</code></li></ol></details><hr><h2 id="26-本课总结">26. 本课总结</h2><p>这一课的重点不是让你去手写 runtime，而是让你能解释常见现象。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>切片本质</td><td>切片头 + 底层数组视图</td></tr><tr><td>切片风险</td><td>共享底层数组、扩容分家、子切片内存滞留</td></tr><tr><td>map 本质</td><td>运行时维护的哈希结构</td></tr><tr><td>map 风险</td><td>顺序不稳定、元素地址不稳定、并发不安全</td></tr><tr><td>接口本质</td><td>动态类型 + 动态值</td></tr><tr><td>接口风险</td><td>nil 判断陷阱、动态分派成本、断言失败</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>切片复制通常不是深拷贝，而是共享底层数组</strong></li><li><strong>map 不是稳定地址容器，所以不能依赖顺序，也不能直接拿元素地址</strong></li><li><strong>接口是否为 nil，要同时看动态类型和动态值</strong></li><li><strong>很多“诡异现象”并不诡异，本质上都是底层表示方式导致的</strong></li></ol><hr><h2 id="27-下一课预告">27. 下一课预告</h2><p>到这里，你已经把 Go 里几个最核心的数据结构和运行时语义摸得更清楚了。下一步，我们就进入真正的性能定位工具。</p><p>下一课：<strong>性能分析与调优入门</strong></p><p>会重点讲：</p><ul><li><code>pprof</code> 是什么</li><li>怎么采集 CPU 和内存分析数据</li><li>火焰图、调用图和热点函数怎么看</li><li>常见性能瓶颈该怎么定位</li><li>调优时应该遵循什么顺序</li></ul><p>学完下一课，你就能从“感觉某段代码慢”进入“拿证据定位瓶颈”的阶段。</p>]]></content>
    
    
    <summary type="html">Go语言系列37</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 36 课：内存与逃逸分析基础</title>
    <link href="https://yjyrichard.github.io/posts/2cf76b95.html"/>
    <id>https://yjyrichard.github.io/posts/2cf76b95.html</id>
    <published>2026-03-22T15:28:11.763Z</published>
    <updated>2026-03-22T15:40:40.040Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 36 课：内存与逃逸分析基础</h1><blockquote><p>学习定位：这是整套 Go 教程的第 36 课，也是阶段六（高级进阶阶段）的第三课。<br>前置要求：已经完成第 35 课，理解 Go 的结构体、指针、函数调用、Benchmark 与反射基础。<br>本课目标：建立 Go 内存行为的基础认知，理解栈与堆的区别、什么是逃逸分析、哪些写法容易导致变量逃逸、值传递与指针传递分别意味着什么，并学会用 <code>go build -gcflags=&quot;-m&quot;</code> 初步观察编译器的逃逸分析结果。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面你已经开始接触性能测试了，也学了项目结构、泛型和反射。接下来要补上一块非常关键的底层认知：<strong>Go 代码到底是怎么分配内存的。</strong></p><p>很多性能相关的问题，最后都会落到下面这些判断上：</p><ul><li>这个变量是在栈上还是堆上？</li><li>为什么我只是返回了一个指针，它就“逃逸”了？</li><li>结构体传值是不是一定比传指针慢？</li><li>这个函数为什么会触发更多 GC？</li><li>为什么反射、接口、闭包有时更容易产生额外分配？</li></ul><p>如果这些问题完全没概念，你做性能优化时很容易出现两种情况：</p><ol><li>靠感觉优化，方向常常不对</li><li>看到 “moved to heap” 这种编译器输出却不知道什么意思</li></ol><p>这节课不是让你一上来就做底层黑魔法，而是帮你建立三个基本判断：</p><ol><li>Go 里变量不只是“有值”，还有“存在哪”</li><li>编译器会根据使用方式决定放栈还是堆</li><li>堆分配通常更贵，因为它可能带来 GC 成本</li></ol><hr><h2 id="2-先建立直觉：栈和堆到底有什么区别">2. 先建立直觉：栈和堆到底有什么区别</h2><h3 id="2-1-栈是什么">2.1 栈是什么</h3><p>可以先把栈理解成：</p><blockquote><p>当前函数调用过程中，临时使用的一块相对轻量、自动管理的内存区域。</p></blockquote><p>典型特点：</p><ul><li>分配和回收很快</li><li>生命周期通常跟函数调用相关</li><li>编译器更容易管理</li></ul><p>例如一个普通函数里的局部变量，如果不会被外部继续使用，就很可能放在栈上。</p><h3 id="2-2-堆是什么">2.2 堆是什么</h3><p>可以先把堆理解成：</p><blockquote><p>生命周期可能超出当前函数调用，需要由运行时统一管理的一块内存区域。</p></blockquote><p>典型特点：</p><ul><li>分配成本通常比栈高</li><li>不能随着当前函数结束直接回收</li><li>需要垃圾回收器（GC）参与管理</li></ul><h3 id="2-3-一个很粗糙但实用的理解">2.3 一个很粗糙但实用的理解</h3><p>你可以先记成：</p><ul><li><strong>栈</strong>：更像“当前函数自己用的临时空间”</li><li><strong>堆</strong>：更像“函数结束后还可能被别人继续用的空间”</li></ul><p>这当然不是完整定义，但对入门判断很有帮助。</p><hr><h2 id="3-不要死记“指针一定在堆上”">3. 不要死记“指针一定在堆上”</h2><p>这是初学者最常见的误解之一。</p><p>很多人一学到指针，就会形成一个错误印象：</p><blockquote><p>只要用了指针，数据就一定在堆上。</p></blockquote><p>这不对。</p><p>Go 里变量放栈还是堆，不是由“你有没有写 <code>*</code> 或 <code>&amp;</code>”单独决定的，而是由：</p><ul><li>变量是否会在函数外继续存活</li><li>编译器能不能证明它只在当前作用域安全使用</li></ul><p>共同决定的。</p><p>也就是说：</p><ul><li>有些用了指针的值仍然可以在栈上</li><li>有些没写指针的值也可能逃逸到堆上</li></ul><p>这点非常重要，因为后面很多判断都建立在这里。</p><hr><h2 id="4-什么是逃逸分析">4. 什么是逃逸分析</h2><h3 id="4-1-先说定义">4.1 先说定义</h3><p>逃逸分析（Escape Analysis）是编译器在编译阶段做的一项分析，用来判断：</p><blockquote><p>一个变量能不能安全地分配在栈上，还是必须放到堆上。</p></blockquote><h3 id="4-2-“逃逸”是什么意思">4.2 “逃逸”是什么意思</h3><p>所谓“逃逸”，你可以先理解成：</p><blockquote><p>这个值的使用范围“逃出了”当前函数栈帧。</p></blockquote><p>例如：</p><ul><li>函数返回了局部变量的地址</li><li>局部变量被闭包捕获，函数结束后还要用</li><li>值被放进某些编译器无法证明局部安全的场景</li></ul><p>这时编译器就可能决定：</p><p><strong>这个变量不能只放栈上，得放堆上。</strong></p><h3 id="4-3-为什么编译器要做这个分析">4.3 为什么编译器要做这个分析</h3><p>因为如果一个变量本来可以安全放栈上，那就没必要上堆。</p><p>好处是：</p><ul><li>分配更快</li><li>回收更简单</li><li>GC 压力更小</li></ul><p>所以逃逸分析本质上是编译器帮你做的一种性能优化与安全判断。</p><hr><h2 id="5-一个最经典的例子：返回局部变量指针">5. 一个最经典的例子：返回局部变量指针</h2><h3 id="5-1-代码">5.1 代码</h3><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="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">NewInt</span><span class="params">()</span></span> *<span class="type">int</span> &#123;</span><br><span class="line">    x := <span class="number">10</span></span><br><span class="line">    <span class="keyword">return</span> &amp;x</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">    p := NewInt()</span><br><span class="line">    fmt.Println(*p)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-为什么这里会逃逸">5.2 为什么这里会逃逸</h3><p><code>x</code> 是 <code>NewInt</code> 的局部变量。</p><p>按正常情况，<code>NewInt</code> 返回后，它的栈帧就该结束了。</p><p>但你返回了 <code>&amp;x</code>，意味着：</p><ul><li><code>main</code> 里还要继续用这个地址</li><li>所以 <code>x</code> 不能随着 <code>NewInt</code> 结束一起消失</li></ul><p>编译器就会把 <code>x</code> 放到堆上。</p><h3 id="5-3-这不是-bug，而是-Go-的正常机制">5.3 这不是 bug，而是 Go 的正常机制</h3><p>很多新手会担心：</p><blockquote><p>“返回局部变量地址不是危险的吗？”</p></blockquote><p>在 C/C++ 里，这种事可能非常危险。</p><p>但 Go 编译器会通过逃逸分析把它安全地放到堆上，所以这段代码是安全的。</p><p>这就是 Go 语言层和底层实现配合的典型例子。</p><hr><h2 id="6-对比：返回值和返回指针，不只是语法差异">6. 对比：返回值和返回指针，不只是语法差异</h2><h3 id="6-1-返回值版本">6.1 返回值版本</h3><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="function"><span class="keyword">func</span> <span class="title">NewPointValue</span><span class="params">()</span></span> Point &#123;</span><br><span class="line">    p := Point&#123;X: <span class="number">10</span>, Y: <span class="number">20</span>&#125;</span><br><span class="line">    <span class="keyword">return</span> p</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-返回指针版本">6.2 返回指针版本</h3><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="function"><span class="keyword">func</span> <span class="title">NewPointPointer</span><span class="params">()</span></span> *Point &#123;</span><br><span class="line">    p := Point&#123;X: <span class="number">10</span>, Y: <span class="number">20</span>&#125;</span><br><span class="line">    <span class="keyword">return</span> &amp;p</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-这两者的差异要怎么理解">6.3 这两者的差异要怎么理解</h3><p>很多人会直觉认为：</p><ul><li>返回值 -&gt; 一定慢，因为要拷贝</li><li>返回指针 -&gt; 一定快，因为只返回地址</li></ul><p>这是非常危险的简单化结论。</p><p>真实情况是：</p><ul><li>返回值可能完全不逃逸，编译器优化后很高效</li><li>返回指针可能导致堆分配和后续 GC 成本</li></ul><p>所以性能不能只看“有没有拷贝”，还要看：</p><ul><li>数据多大</li><li>生命周期多长</li><li>是否引入堆分配</li><li>是否影响缓存局部性和 GC</li></ul><h3 id="6-4-一条很实用的经验">6.4 一条很实用的经验</h3><p><strong>小而短命的数据，传值常常并不差，甚至更好。</strong></p><p>所以不要一上来就把所有结构体都改成指针。</p><hr><h2 id="7-怎么看逃逸分析结果">7. 怎么看逃逸分析结果</h2><p>Go 编译器可以把分析结果打印出来。</p><h3 id="7-1-常用命令">7.1 常用命令</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m&quot;</span></span><br></pre></td></tr></table></figure><p>如果想看更多信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m -m&quot;</span></span><br></pre></td></tr></table></figure><h3 id="7-2-示例代码">7.2 示例代码</h3><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="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">NewInt</span><span class="params">()</span></span> *<span class="type">int</span> &#123;</span><br><span class="line">    x := <span class="number">10</span></span><br><span class="line">    <span class="keyword">return</span> &amp;x</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">    _ = NewInt()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m&quot;</span></span><br></pre></td></tr></table></figure><p>可能看到类似输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./main.go:4:2: moved to heap: x</span><br></pre></td></tr></table></figure><h3 id="7-3-怎么读这类输出">7.3 怎么读这类输出</h3><p><code>moved to heap: x</code> 的意思就是：</p><ul><li>变量 <code>x</code> 原本是局部变量</li><li>但编译器判断它不能安全只待在栈上</li><li>所以把它放到了堆上</li></ul><p>要注意：</p><ul><li>具体输出内容会随 Go 版本变化</li><li>不同平台和编译优化也会影响细节</li></ul><p>所以你要读的是“结论方向”，不是死记每一行字面格式。</p><hr><h2 id="8-不返回指针，也可能逃逸">8. 不返回指针，也可能逃逸</h2><p>这是另一个很重要的认知点。</p><h3 id="8-1-例子：放进接口">8.1 例子：放进接口</h3><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></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="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</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">PrintAny</span><span class="params">(v any)</span></span> &#123;</span><br><span class="line">    fmt.Println(v)</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">    u := User&#123;Name: <span class="string">&quot;张三&quot;</span>, Age: <span class="number">18</span>&#125;</span><br><span class="line">    PrintAny(u)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里虽然没有显式返回指针，但把值传到接口、再交给 <code>fmt.Println</code> 之类的函数时，可能会引入逃逸。</p><h3 id="8-2-为什么会这样">8.2 为什么会这样</h3><p>因为接口值会携带：</p><ul><li>动态类型信息</li><li>动态值信息</li></ul><p>而具体是否逃逸，取决于编译器能否证明这个值不会在更长生命周期里被使用。</p><h3 id="8-3-这里最重要的不是背规则，而是建立警觉">8.3 这里最重要的不是背规则，而是建立警觉</h3><p>你要知道：</p><ul><li>逃逸不只是“返回地址”这一种情况</li><li>接口、反射、闭包、goroutine 都可能影响逃逸分析结果</li></ul><hr><h2 id="9-闭包为什么常常会导致逃逸">9. 闭包为什么常常会导致逃逸</h2><h3 id="9-1-先看代码">9.1 先看代码</h3><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></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">Counter</span><span class="params">()</span></span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    n := <span class="number">0</span></span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        n++</span><br><span class="line">        <span class="keyword">return</span> n</span><br><span class="line">    &#125;</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">    next := Counter()</span><br><span class="line">    fmt.Println(next())</span><br><span class="line">    fmt.Println(next())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-为什么-n-容易逃逸">9.2 为什么 <code>n</code> 容易逃逸</h3><p>因为 <code>n</code> 本来是 <code>Counter</code> 的局部变量。</p><p>但返回的匿名函数在 <code>Counter</code> 结束后还会继续使用它。</p><p>所以：</p><ul><li><code>n</code> 的生命周期已经超过了 <code>Counter</code> 调用本身</li><li>编译器通常会让它放到堆上</li></ul><h3 id="9-3-这和前面的“返回局部变量指针”本质类似">9.3 这和前面的“返回局部变量指针”本质类似</h3><p>本质都是：</p><ul><li>当前函数结束了</li><li>但某个局部值还得继续活着</li></ul><p>这就是逃逸。</p><hr><h2 id="10-goroutine-也会影响生命周期判断">10. goroutine 也会影响生命周期判断</h2><h3 id="10-1-例子">10.1 例子</h3><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"><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">    x := <span class="number">42</span></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">        <span class="built_in">println</span>(x)</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-2-为什么这里要小心">10.2 为什么这里要小心</h3><p>因为 goroutine 的执行时间不一定和当前函数同步。</p><p>编译器看到：</p><ul><li><code>x</code> 被另一个 goroutine 使用</li><li>这个使用可能持续到当前作用域结束之后</li></ul><p>于是它更可能把相关变量放到堆上。</p><h3 id="10-3-这也是并发代码更容易带来额外分配的原因之一">10.3 这也是并发代码更容易带来额外分配的原因之一</h3><p>并发不仅带来调度、同步等成本，也常常让变量生命周期更复杂，从而影响逃逸分析。</p><hr><h2 id="11-切片、map、channel-本身也是引用语义风格的值">11. 切片、map、channel 本身也是引用语义风格的值</h2><p>这部分很容易让人理解混乱，所以要单独讲。</p><h3 id="11-1-它们是值，但底层指向共享数据">11.1 它们是值，但底层指向共享数据</h3><p>例如切片：</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">s := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br></pre></td></tr></table></figure><p>切片变量本身是一个小结构，通常包含：</p><ul><li>指向底层数组的指针</li><li>长度</li><li>容量</li></ul><p>也就是说：</p><ul><li>切片变量本身可以被复制</li><li>但多个切片值可能共享同一块底层数组</li></ul><h3 id="11-2-这和逃逸分析有什么关系">11.2 这和逃逸分析有什么关系</h3><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="function"><span class="keyword">func</span> <span class="title">MakeSlice</span><span class="params">()</span></span> []<span class="type">int</span> &#123;</span><br><span class="line">    s := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">100</span>)</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里返回的是切片值，不是指针，但切片底层数组显然要在函数外继续存在，所以底层数据会有对应的堆分配考虑。</p><h3 id="11-3-一个实用理解">11.3 一个实用理解</h3><p>不要把“有没有显式写指针”当成内存行为唯一判断依据。</p><p>Go 里很多复合类型即使语法上不是 <code>*T</code>，底层也会涉及共享数据和引用式行为。</p><hr><h2 id="12-结构体传值-vs-传指针，正确比较方式是什么">12. 结构体传值 vs 传指针，正确比较方式是什么</h2><p>这是 Go 里最容易被经验帖误导的话题之一。</p><h3 id="12-1-传值的特点">12.1 传值的特点</h3><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="function"><span class="keyword">func</span> <span class="title">Process</span><span class="params">(u User)</span></span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>特点：</p><ul><li>会复制一份值</li><li>调用方和被调方互不影响</li><li>更容易理解和推理</li></ul><h3 id="12-2-传指针的特点">12.2 传指针的特点</h3><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="function"><span class="keyword">func</span> <span class="title">Process</span><span class="params">(u *User)</span></span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>特点：</p><ul><li>传递的是地址</li><li>可以修改原对象</li><li>可能减少大对象拷贝</li><li>但也可能引入共享状态、逃逸和 GC 成本</li></ul><h3 id="12-3-该怎么选">12.3 该怎么选</h3><p>不要问：</p><blockquote><p>“传值好还是传指针好？”</p></blockquote><p>而要问：</p><ol><li>这个类型大不大？</li><li>我需不需要修改原对象？</li><li>生命周期会不会因此变长？</li><li>这样写会不会让 API 更难理解？</li></ol><h3 id="12-4-一个很实用的经验">12.4 一个很实用的经验</h3><p>对于：</p><ul><li>小结构体</li><li>不需要修改原值</li><li>语义上像普通数据记录</li></ul><p>优先传值通常没问题。</p><p>而对于：</p><ul><li>很大的结构体</li><li>需要原地修改</li><li>需要避免大量复制</li></ul><p>传指针可能更合适。</p><p>但最后仍然要靠测试和 Benchmark，而不是只靠口号。</p><hr><h2 id="13-堆分配为什么常常更贵">13. 堆分配为什么常常更贵</h2><p>理解这一点，才能明白为什么大家总说“减少逃逸有利于性能”。</p><h3 id="13-1-分配本身更复杂">13.1 分配本身更复杂</h3><p>栈上的分配通常可以非常简单，因为跟函数调用过程强相关。</p><p>堆上的分配则需要运行时管理，通常更重。</p><h3 id="13-2-回收要靠-GC">13.2 回收要靠 GC</h3><p>如果一个对象在堆上，等它不用了，并不能像栈那样随着函数退出自然回收。</p><p>它需要等待垃圾回收器识别“已经不可达”后再回收。</p><p>这意味着：</p><ul><li>GC 需要扫描更多对象</li><li>GC 压力会上升</li><li>程序整体吞吐可能受影响</li></ul><h3 id="13-3-大量小对象尤其容易带来问题">13.3 大量小对象尤其容易带来问题</h3><p>很多时候真正拖慢程序的，不是某一个超大的对象，而是：</p><ul><li>高频率创建</li><li>生命周期很短</li><li>数量又很多</li></ul><p>这种模式会让 GC 很忙。</p><p>所以优化时常见目标之一就是：</p><p><strong>减少不必要的短命堆对象。</strong></p><hr><h2 id="14-反射、接口、fmt-常常更容易带来分配">14. 反射、接口、fmt 常常更容易带来分配</h2><p>这部分你已经学过前置知识，现在可以把它们串起来看了。</p><h3 id="14-1-为什么-fmt-Println-有时会带来更多开销">14.1 为什么 <code>fmt.Println</code> 有时会带来更多开销</h3><p>因为它是高度通用的：</p><ul><li>接收可变参数</li><li>处理接口值</li><li>做格式化</li></ul><p>这背后可能涉及：</p><ul><li>接口装箱</li><li>额外分配</li><li>反射或动态处理路径</li></ul><h3 id="14-2-为什么反射常常更重">14.2 为什么反射常常更重</h3><p>因为反射本来就是动态机制，需要：</p><ul><li>包装类型和值</li><li>运行时判断</li><li>可能产生更多间接访问</li></ul><h3 id="14-3-这里的核心结论不是“不能用”">14.3 这里的核心结论不是“不能用”</h3><p>而是：</p><ul><li>在普通业务里，这些额外成本通常可以接受</li><li>在高频热点路径里，就要更谨慎</li></ul><p>这也是为什么性能优化总强调：</p><p><strong>先找热点，再做针对性判断。</strong></p><hr><h2 id="15-一个最重要的工具：用编译器输出验证，不靠猜">15. 一个最重要的工具：用编译器输出验证，不靠猜</h2><p>前面都是概念，这里要落到实际动作上。</p><h3 id="15-1-看逃逸">15.1 看逃逸</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m&quot;</span></span><br></pre></td></tr></table></figure><h3 id="15-2-看性能">15.2 看性能</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -benchmem</span><br></pre></td></tr></table></figure><h3 id="15-3-两者结合才有意义">15.3 两者结合才有意义</h3><p>例如你改了一个函数：</p><ul><li>少了一个指针返回</li><li>改成传值</li></ul><p>这时你应该：</p><ol><li>用 <code>-gcflags=&quot;-m&quot;</code> 看编译器是否减少了逃逸</li><li>用 Benchmark 看实际 <code>ns/op</code>、<code>B/op</code>、<code>allocs/op</code> 有没有变化</li></ol><p>不要只看到一条 <code>moved to heap</code> 就马上下结论。</p><p>因为：</p><ul><li>有逃逸不一定就是坏事</li><li>没逃逸也不一定整体更快</li></ul><p>最终还是要回到数据。</p><hr><h2 id="16-一个完整示例：对比值返回和指针返回">16. 一个完整示例：对比值返回和指针返回</h2><h3 id="16-1-示例代码">16.1 示例代码</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> point</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Point <span class="keyword">struct</span> &#123;</span><br><span class="line">    X <span class="type">int</span></span><br><span class="line">    Y <span class="type">int</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">NewPointValue</span><span class="params">(x, y <span class="type">int</span>)</span></span> Point &#123;</span><br><span class="line">    p := Point&#123;X: x, Y: y&#125;</span><br><span class="line">    <span class="keyword">return</span> p</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">NewPointPointer</span><span class="params">(x, y <span class="type">int</span>)</span></span> *Point &#123;</span><br><span class="line">    p := Point&#123;X: x, Y: y&#125;</span><br><span class="line">    <span class="keyword">return</span> &amp;p</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="16-2-你可以怎么分析">16.2 你可以怎么分析</h3><p>第一步，看逃逸：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m&quot;</span></span><br></pre></td></tr></table></figure><p>第二步，写 Benchmark：</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="function"><span class="keyword">func</span> <span class="title">BenchmarkNewPointValue</span><span class="params">(b *testing.B)</span></span> &#123;</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">        _ = NewPointValue(<span class="number">10</span>, <span class="number">20</span>)</span><br><span class="line">    &#125;</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">BenchmarkNewPointPointer</span><span class="params">(b *testing.B)</span></span> &#123;</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">        _ = NewPointPointer(<span class="number">10</span>, <span class="number">20</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>第三步，看结果：</p><ul><li><code>ns/op</code></li><li><code>B/op</code></li><li><code>allocs/op</code></li></ul><h3 id="16-3-你真正该学到什么">16.3 你真正该学到什么</h3><p>不是“返回值永远更快”或者“返回指针永远更快”，而是：</p><p><strong>这类问题必须结合逃逸分析和基准测试一起判断。</strong></p><hr><h2 id="17-常见坑总结">17. 常见坑总结</h2><h3 id="17-1-看到指针就说“肯定更快”">17.1 看到指针就说“肯定更快”</h3><p>不对。</p><p>指针可能减少拷贝，但也可能：</p><ul><li>让对象逃逸到堆上</li><li>增加 GC 压力</li><li>增加共享状态复杂度</li></ul><h3 id="17-2-看到传值就说“肯定复制很慢”">17.2 看到传值就说“肯定复制很慢”</h3><p>也不对。</p><p>对于小对象，值复制常常便宜得多，而且更利于局部性和简单性。</p><h3 id="17-3-把逃逸分析当成“越少越神”">17.3 把逃逸分析当成“越少越神”</h3><p>有些逃逸是合理且必要的。</p><p>例如：</p><ul><li>返回对象指针作为 API 设计的一部分</li><li>闭包天然需要延长变量生命周期</li></ul><p>目标不是“绝对零逃逸”，而是：</p><p><strong>减少不必要的逃逸。</strong></p><h3 id="17-4-只盯着-moved-to-heap">17.4 只盯着 <code>moved to heap</code></h3><p>编译器输出只是线索，不是最终性能结论。</p><p>还要结合：</p><ul><li>Benchmark</li><li>内存分配数据</li><li>实际热点路径</li></ul><h3 id="17-5-为了避免逃逸把代码写得很难懂">17.5 为了避免逃逸把代码写得很难懂</h3><p>如果为了省一两个分配，把代码改得非常绕、可维护性很差，那通常不值得。</p><p>性能优化必须考虑成本收益比。</p><h3 id="17-6-忘了接口、反射、闭包、goroutine-都可能影响逃逸">17.6 忘了接口、反射、闭包、goroutine 都可能影响逃逸</h3><p>很多人只盯着“返回指针”，却忽略了这些更隐蔽的场景。</p><p>真正成熟的判断应该更全面。</p><hr><h2 id="18-本课练习">18. 本课练习</h2><h3 id="练习-1：观察返回指针导致的逃逸">练习 1：观察返回指针导致的逃逸</h3><p>写两个函数：</p><ul><li>一个返回 <code>int</code></li><li>一个返回 <code>*int</code></li></ul><p>然后用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags=<span class="string">&quot;-m&quot;</span></span><br></pre></td></tr></table></figure><p>观察它们的编译器输出差异。</p><hr><h3 id="练习-2：闭包捕获变量">练习 2：闭包捕获变量</h3><p>写一个返回匿名函数的计数器：</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="function"><span class="keyword">func</span> <span class="title">Counter</span><span class="params">()</span></span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> <span class="type">int</span></span><br></pre></td></tr></table></figure><p>然后观察：</p><ul><li>计数变量会不会逃逸</li><li>为什么会逃逸</li></ul><hr><h3 id="练习-3：值传递-vs-指针传递">练习 3：值传递 vs 指针传递</h3><p>定义一个有 5～8 个字段的结构体，分别写：</p><ul><li>传值版本处理函数</li><li>传指针版本处理函数</li></ul><p>再写 Benchmark 对比它们的：</p><ul><li><code>ns/op</code></li><li><code>B/op</code></li><li><code>allocs/op</code></li></ul><hr><h3 id="练习-4：接口装箱观察">练习 4：接口装箱观察</h3><p>写两个函数：</p><ul><li>一个直接处理 <code>int</code></li><li>一个接收 <code>any</code></li></ul><p>分别在循环中调用，观察 Benchmark 和逃逸分析输出，思考接口通用性带来的代价。</p><hr><h3 id="练习-5：反射路径观察">练习 5：反射路径观察</h3><p>写两个版本的字段读取逻辑：</p><ol><li>直接访问结构体字段</li><li>用反射按字段名读取</li></ol><p>对比它们的可读性、性能以及分配情况。</p><hr><h2 id="19-自测题">19. 自测题</h2><h3 id="19-1-概念题">19.1 概念题</h3><ol><li>栈和堆最核心的区别是什么？</li><li>什么是逃逸分析？编译器为什么要做它？</li><li>为什么“用了指针就一定在堆上”是错误说法？</li><li>返回局部变量地址为什么在 Go 里是安全的？</li><li>闭包为什么常常会导致局部变量逃逸？</li><li>为什么 goroutine 会让变量生命周期判断变复杂？</li><li>为什么说减少堆分配通常有利于性能？</li><li><code>go build -gcflags=&quot;-m&quot;</code> 这条命令主要是干什么的？</li></ol><h3 id="19-2-代码阅读题">19.2 代码阅读题</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BuildUser</span><span class="params">(name <span class="type">string</span>)</span></span> *User &#123;</span><br><span class="line">    u := User&#123;Name: name, Age: <span class="number">18</span>&#125;</span><br><span class="line">    <span class="keyword">return</span> &amp;u</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>最可能逃逸的是局部变量 <code>u</code>。</p><p>原因是：</p><ul><li><code>u</code> 是 <code>BuildUser</code> 内部定义的局部变量</li><li>但函数返回了 <code>&amp;u</code></li><li>调用方在函数结束后还要继续使用这个地址</li></ul><p>因此编译器通常会把 <code>u</code> 放到堆上，而不是栈上。</p><p>这正是逃逸分析最经典的场景之一。</p></details><hr><h2 id="20-本课总结">20. 本课总结</h2><p>这一课的核心，不是让你背一堆底层术语，而是建立对 Go 内存行为的基本直觉。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>栈 vs 堆</td><td>栈更轻量，堆需要运行时和 GC 管理</td></tr><tr><td>逃逸分析</td><td>编译器判断变量该放栈还是堆</td></tr><tr><td>常见逃逸场景</td><td>返回指针、闭包捕获、goroutine、部分接口/反射场景</td></tr><tr><td>值 vs 指针</td><td>不能只看拷贝，还要看逃逸、生命周期和 GC</td></tr><tr><td>观察工具</td><td><code>go build -gcflags=&quot;-m&quot;</code> + Benchmark</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>变量放栈还是堆，是编译器根据使用方式决定的，不是只看有没有指针</strong></li><li><strong>堆分配通常更贵，因为它可能带来额外 GC 成本</strong></li><li><strong>性能判断不能靠经验口号，必须结合逃逸分析和 Benchmark</strong></li><li><strong>优化目标不是“绝对零逃逸”，而是减少不必要的堆分配和复杂度</strong></li></ol><hr><h2 id="21-下一课预告">21. 下一课预告</h2><p>你已经开始理解 Go 的内存行为了。下一步，我们要把视角继续往下压，去看几个最核心数据结构的底层特征。</p><p>下一课：<strong>深入理解切片、map 与接口底层</strong></p><p>会重点讲：</p><ul><li>切片的底层结构和扩容行为</li><li>map 的一些关键特征与使用风险</li><li>接口值的内部表示</li><li>为什么这些底层细节会影响性能和正确性</li><li>怎么用这些认知解释常见现象</li></ul><p>学完下一课，你对 Go 运行时行为的理解会明显更扎实。</p>]]></content>
    
    
    <summary type="html">Go语言系列36</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 35 课：反射入门</title>
    <link href="https://yjyrichard.github.io/posts/357161ab.html"/>
    <id>https://yjyrichard.github.io/posts/357161ab.html</id>
    <published>2026-03-22T15:28:11.759Z</published>
    <updated>2026-03-22T15:40:40.031Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 35 课：反射入门</h1><blockquote><p>学习定位：这是整套 Go 教程的第 35 课，也是阶段六（高级进阶阶段）的第二课。<br>前置要求：已经完成第 34 课，理解 Go 的类型系统、接口、结构体、方法和泛型基础。<br>本课目标：理解 Go 反射的核心作用，掌握 <code>reflect.Type</code>、<code>reflect.Value</code>、<code>Kind</code>、<code>Elem</code>、<code>CanSet</code> 等关键概念，学会在运行时读取类型信息、遍历结构体字段、读取标签、修改可设置值，并知道反射的代价、适用场景和常见坑。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面的 Go 代码，大多数都是“编译期就知道类型”的。</p><p>例如：</p><ul><li><code>var n int = 10</code></li><li><code>user.Name</code></li><li><code>tasks[0].Title</code></li></ul><p>编译器在你写代码的时候，就已经知道：</p><ul><li>变量是什么类型</li><li>有哪些字段</li><li>有哪些方法</li></ul><p>但真实项目里，有一类问题在编译期并不知道全部信息：</p><ul><li>传进来的到底是不是结构体</li><li>这个结构体有哪些字段</li><li>字段上有没有 <code>json:&quot;name&quot;</code> 这种标签</li><li>我想根据字符串 <code>&quot;Name&quot;</code> 动态找到字段并赋值</li><li>我写的是通用框架代码，不知道用户会传什么类型</li></ul><p>这时候，普通静态写法就不够了。</p><p><strong>反射解决的问题就是：让程序在运行时查看和操作类型信息。</strong></p><p>这听起来很强，但也很危险，因为它会带来：</p><ul><li>代码更绕</li><li>性能更差</li><li>出错更隐蔽</li></ul><p>所以这节课要讲清楚两件事：</p><ol><li>反射到底能做什么</li><li>为什么它必须谨慎使用</li></ol><hr><h2 id="2-先建立直觉：什么叫“运行时看类型”">2. 先建立直觉：什么叫“运行时看类型”</h2><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></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;reflect&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="keyword">var</span> x <span class="type">int</span> = <span class="number">42</span></span><br><span class="line"></span><br><span class="line">    t := reflect.TypeOf(x)</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line"></span><br><span class="line">    fmt.Println(t)        <span class="comment">// int</span></span><br><span class="line">    fmt.Println(t.Kind()) <span class="comment">// int</span></span><br><span class="line">    fmt.Println(v)        <span class="comment">// 42</span></span><br><span class="line">    fmt.Println(v.Kind()) <span class="comment">// int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里做了什么？</p><ul><li><code>reflect.TypeOf(x)</code>：拿到变量的类型描述</li><li><code>reflect.ValueOf(x)</code>：拿到变量当前的值描述</li></ul><p>也就是说，反射把“类型”和“值”都包装成了可在运行时分析的对象。</p><p>你可以先把它记成：</p><ul><li><code>Type</code> 关注“它是什么”</li><li><code>Value</code> 关注“它现在的值是什么”</li></ul><hr><h2 id="3-reflect-Type-和-reflect-Value-是两块核心地基">3. <code>reflect.Type</code> 和 <code>reflect.Value</code> 是两块核心地基</h2><h3 id="3-1-reflect-Type">3.1 <code>reflect.Type</code></h3><p><code>reflect.Type</code> 用来描述类型信息。</p><p>常见操作：</p><ul><li>看类型名</li><li>看种类</li><li>看字段</li><li>看方法</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</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">    u := User&#123;Name: <span class="string">&quot;张三&quot;</span>, Age: <span class="number">18</span>&#125;</span><br><span class="line">    t := reflect.TypeOf(u)</span><br><span class="line"></span><br><span class="line">    fmt.Println(t.Name())    <span class="comment">// User</span></span><br><span class="line">    fmt.Println(t.Kind())    <span class="comment">// struct</span></span><br><span class="line">    fmt.Println(t.NumField()) <span class="comment">// 2</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-reflect-Value">3.2 <code>reflect.Value</code></h3><p><code>reflect.Value</code> 用来描述值。</p><p>常见操作：</p><ul><li>读取值</li><li>判断值类型</li><li>取字段值</li><li>在满足条件时修改值</li></ul><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></pre></td><td class="code"><pre><span class="line">u := User&#123;Name: <span class="string">&quot;张三&quot;</span>, Age: <span class="number">18</span>&#125;</span><br><span class="line">v := reflect.ValueOf(u)</span><br><span class="line"></span><br><span class="line">fmt.Println(v.Kind())                 <span class="comment">// struct</span></span><br><span class="line">fmt.Println(v.FieldByName(<span class="string">&quot;Name&quot;</span>))    <span class="comment">// 张三</span></span><br><span class="line">fmt.Println(v.FieldByName(<span class="string">&quot;Age&quot;</span>).Int()) <span class="comment">// 18</span></span><br></pre></td></tr></table></figure><h3 id="3-3-一句话区分">3.3 一句话区分</h3><p>你可以先这样记：</p><ul><li><code>Type</code> 更像“说明书”</li><li><code>Value</code> 更像“当前实例”</li></ul><p>很多反射代码，本质上就是在 <code>Type</code> 和 <code>Value</code> 之间来回切换。</p><hr><h2 id="4-Type-不等于-Kind">4. <code>Type</code> 不等于 <code>Kind</code></h2><p>这是反射里第一个很重要、也很容易混的点。</p><h3 id="4-1-Type-是具体类型">4.1 <code>Type</code> 是具体类型</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> MyInt <span class="type">int</span></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span>&#123;&#125;</span><br></pre></td></tr></table></figure><p>它们的 <code>Type</code> 分别是：</p><ul><li><code>MyInt</code></li><li><code>User</code></li></ul><h3 id="4-2-Kind-是底层类别">4.2 <code>Kind</code> 是底层类别</h3><p>例如：</p><ul><li><code>int</code></li><li><code>string</code></li><li><code>struct</code></li><li><code>slice</code></li><li><code>map</code></li><li><code>ptr</code></li></ul><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> MyInt <span class="type">int</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">var</span> x MyInt = <span class="number">10</span></span><br><span class="line"></span><br><span class="line">    t := reflect.TypeOf(x)</span><br><span class="line">    fmt.Println(t.Name()) <span class="comment">// MyInt</span></span><br><span class="line">    fmt.Println(t.Kind()) <span class="comment">// int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里：</p><ul><li><code>Name()</code> 是 <code>MyInt</code></li><li><code>Kind()</code> 是 <code>int</code></li></ul><h3 id="4-3-为什么要区分这两个">4.3 为什么要区分这两个</h3><p>因为很多反射逻辑判断的是“它属于哪一类”。</p><p>比如：</p><ul><li>如果 <code>Kind() == reflect.Struct</code>，那就可以遍历字段</li><li>如果 <code>Kind() == reflect.Slice</code>，那就可以遍历元素</li><li>如果 <code>Kind() == reflect.Ptr</code>，那就应该先 <code>Elem()</code></li></ul><p>所以：</p><ul><li>想知道具体类型是谁，看 <code>Type</code></li><li>想知道应该怎么处理它，看 <code>Kind</code></li></ul><hr><h2 id="5-读取基本类型信息">5. 读取基本类型信息</h2><p>下面先把最常见的几个 API 过一遍。</p><h3 id="5-1-读取类型名和种类">5.1 读取类型名和种类</h3><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</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">    u := User&#123;Name: <span class="string">&quot;李四&quot;</span>&#125;</span><br><span class="line">    t := reflect.TypeOf(u)</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;类型名:&quot;</span>, t.Name())   <span class="comment">// User</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;种类:&quot;</span>, t.Kind())     <span class="comment">// struct</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;完整类型:&quot;</span>, t.String()) <span class="comment">// main.User</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-判断是不是某种类别">5.2 判断是不是某种类别</h3><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">PrintKind</span><span class="params">(x any)</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(x)</span><br><span class="line">    <span class="keyword">switch</span> t.Kind() &#123;</span><br><span class="line">    <span class="keyword">case</span> reflect.Int:</span><br><span class="line">        fmt.Println(<span class="string">&quot;这是整数&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> reflect.String:</span><br><span class="line">        fmt.Println(<span class="string">&quot;这是字符串&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> reflect.Struct:</span><br><span class="line">        fmt.Println(<span class="string">&quot;这是结构体&quot;</span>)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;其他类型&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-读取值">5.3 读取值</h3><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="function"><span class="keyword">func</span> <span class="title">PrintValue</span><span class="params">(x any)</span></span> &#123;</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line">    fmt.Println(<span class="string">&quot;值:&quot;</span>, v)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不过要注意：</p><p><code>reflect.Value</code> 不是普通值本身，而是一个“反射包装对象”。你通常还要调用它的方法拿到具体内容。</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></pre></td><td class="code"><pre><span class="line">v := reflect.ValueOf(<span class="number">123</span>)</span><br><span class="line">fmt.Println(v.Int()) <span class="comment">// 123</span></span><br></pre></td></tr></table></figure><hr><h2 id="6-不同-Kind-取值方式不同">6. 不同 <code>Kind</code> 取值方式不同</h2><p>这是反射里最容易踩坑的一类问题。</p><h3 id="6-1-整数用-Int">6.1 整数用 <code>Int()</code></h3><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">v := reflect.ValueOf(<span class="number">123</span>)</span><br><span class="line">fmt.Println(v.Int())</span><br></pre></td></tr></table></figure><h3 id="6-2-字符串用-String">6.2 字符串用 <code>String()</code></h3><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">v := reflect.ValueOf(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">fmt.Println(v.String())</span><br></pre></td></tr></table></figure><h3 id="6-3-布尔值用-Bool">6.3 布尔值用 <code>Bool()</code></h3><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">v := reflect.ValueOf(<span class="literal">true</span>)</span><br><span class="line">fmt.Println(v.Bool())</span><br></pre></td></tr></table></figure><h3 id="6-4-浮点数用-Float">6.4 浮点数用 <code>Float()</code></h3><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">v := reflect.ValueOf(<span class="number">3.14</span>)</span><br><span class="line">fmt.Println(v.Float())</span><br></pre></td></tr></table></figure><h3 id="6-5-错误示例">6.5 错误示例</h3><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">v := reflect.ValueOf(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">fmt.Println(v.Int()) <span class="comment">// panic</span></span><br></pre></td></tr></table></figure><p>为什么？</p><p>因为 <code>v</code> 的底层种类是 <code>string</code>，你却按整数去取。</p><p>所以反射里一个非常重要的习惯是：</p><blockquote><p>先看 <code>Kind()</code>，再决定调用哪个取值方法。</p></blockquote><hr><h2 id="7-指针和-Elem-：反射里最关键的入口之一">7. 指针和 <code>Elem()</code>：反射里最关键的入口之一</h2><p>很多反射代码看着复杂，本质都绕不开 <code>Elem()</code>。</p><h3 id="7-1-什么是-Elem">7.1 什么是 <code>Elem()</code></h3><p>如果一个值是：</p><ul><li>指针</li><li>接口</li><li>切片元素</li><li>数组元素</li></ul><p>那你经常需要“进入下一层”。</p><p>对于指针来说，<code>Elem()</code> 表示“指针指向的那个值”。</p><h3 id="7-2-先看一个例子">7.2 先看一个例子</h3><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></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;reflect&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">    x := <span class="number">10</span></span><br><span class="line"></span><br><span class="line">    vp := reflect.ValueOf(&amp;x)</span><br><span class="line">    fmt.Println(vp.Kind()) <span class="comment">// ptr</span></span><br><span class="line"></span><br><span class="line">    ve := vp.Elem()</span><br><span class="line">    fmt.Println(ve.Kind()) <span class="comment">// int</span></span><br><span class="line">    fmt.Println(ve.Int())  <span class="comment">// 10</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里：</p><ul><li><code>&amp;x</code> 的 <code>Kind</code> 是 <code>ptr</code></li><li><code>vp.Elem()</code> 才是它指向的那个 <code>int</code></li></ul><h3 id="7-3-为什么-Elem-这么重要">7.3 为什么 <code>Elem()</code> 这么重要</h3><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">x := <span class="number">10</span></span><br><span class="line">v := reflect.ValueOf(&amp;x).Elem()</span><br><span class="line">v.SetInt(<span class="number">99</span>)</span><br><span class="line">fmt.Println(x) <span class="comment">// 99</span></span><br></pre></td></tr></table></figure><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">v := reflect.ValueOf(x)</span><br><span class="line">v.SetInt(<span class="number">99</span>)</span><br></pre></td></tr></table></figure><p>会直接 panic，因为它不是可设置的值。</p><hr><h2 id="8-修改值：CanSet-是关键判断">8. 修改值：<code>CanSet</code> 是关键判断</h2><p>反射不仅能看，还能改，但不是所有值都能改。</p><h3 id="8-1-先看失败例子">8.1 先看失败例子</h3><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></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;reflect&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">    x := <span class="number">10</span></span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line"></span><br><span class="line">    fmt.Println(v.CanSet()) <span class="comment">// false</span></span><br><span class="line">    <span class="comment">// v.SetInt(20)         // panic</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么不能改？</p><p>因为 <code>reflect.ValueOf(x)</code> 拿到的是 <code>x</code> 的一个副本信息，不是可回写的原对象。</p><h3 id="8-2-正确写法：传指针，再-Elem">8.2 正确写法：传指针，再 <code>Elem()</code></h3><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"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    x := <span class="number">10</span></span><br><span class="line">    v := reflect.ValueOf(&amp;x).Elem()</span><br><span class="line"></span><br><span class="line">    fmt.Println(v.CanSet()) <span class="comment">// true</span></span><br><span class="line">    v.SetInt(<span class="number">20</span>)</span><br><span class="line">    fmt.Println(x) <span class="comment">// 20</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-3-一个通用示例">8.3 一个通用示例</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetIntValue</span><span class="params">(ptr any, newValue <span class="type">int64</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    v := reflect.ValueOf(ptr)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> v.Kind() != reflect.Ptr &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要传入指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    elem := v.Elem()</span><br><span class="line">    <span class="keyword">if</span> elem.Kind() != reflect.Int &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要指向 int 的指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !elem.CanSet() &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;目标不可修改&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    elem.SetInt(newValue)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> x <span class="type">int</span> = <span class="number">10</span></span><br><span class="line">err := SetIntValue(&amp;x, <span class="number">99</span>)</span><br><span class="line">fmt.Println(x, err) <span class="comment">// 99 &lt;nil&gt;</span></span><br></pre></td></tr></table></figure><h3 id="8-4-一个经验口诀">8.4 一个经验口诀</h3><p>想通过反射修改值，通常要满足三件事：</p><ol><li>传的是指针</li><li>找到了正确的目标值</li><li><code>CanSet()</code> 为 <code>true</code></li></ol><hr><h2 id="9-结构体反射：最常见、最实用">9. 结构体反射：最常见、最实用</h2><p>反射在实际项目里最常见的对象不是基本类型，而是结构体。</p><p>因为很多框架都需要：</p><ul><li>看结构体字段</li><li>看字段标签</li><li>按字段名读写</li></ul><h3 id="9-1-遍历结构体字段">9.1 遍历结构体字段</h3><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;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</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">    u := User&#123;Name: <span class="string">&quot;王五&quot;</span>, Age: <span class="number">20</span>&#125;</span><br><span class="line"></span><br><span class="line">    t := reflect.TypeOf(u)</span><br><span class="line">    v := reflect.ValueOf(u)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; t.NumField(); i++ &#123;</span><br><span class="line">        fieldType := t.Field(i)</span><br><span class="line">        fieldValue := v.Field(i)</span><br><span class="line"></span><br><span class="line">        fmt.Println(<span class="string">&quot;字段名:&quot;</span>, fieldType.Name)</span><br><span class="line">        fmt.Println(<span class="string">&quot;字段类型:&quot;</span>, fieldType.Type)</span><br><span class="line">        fmt.Println(<span class="string">&quot;字段值:&quot;</span>, fieldValue)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出大致会是：</p><figure class="highlight text"><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">字段名: Name</span><br><span class="line">字段类型: string</span><br><span class="line">字段值: 王五</span><br><span class="line">字段名: Age</span><br><span class="line">字段类型: int</span><br><span class="line">字段值: 20</span><br></pre></td></tr></table></figure><h3 id="9-2-按字段名查找">9.2 按字段名查找</h3><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">u := User&#123;Name: <span class="string">&quot;王五&quot;</span>, Age: <span class="number">20</span>&#125;</span><br><span class="line">v := reflect.ValueOf(u)</span><br><span class="line"></span><br><span class="line">fmt.Println(v.FieldByName(<span class="string">&quot;Name&quot;</span>)) <span class="comment">// 王五</span></span><br><span class="line">fmt.Println(v.FieldByName(<span class="string">&quot;Age&quot;</span>))  <span class="comment">// 20</span></span><br></pre></td></tr></table></figure><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">f := v.FieldByName(<span class="string">&quot;Email&quot;</span>)</span><br><span class="line">fmt.Println(f.IsValid()) <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>所以按名字找字段时，最好先判断 <code>IsValid()</code>。</p><hr><h2 id="10-读取结构体标签：这就是很多框架的基础">10. 读取结构体标签：这就是很多框架的基础</h2><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">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span> <span class="string">`json:&quot;name&quot; db:&quot;user_name&quot;`</span></span><br><span class="line">    Age  <span class="type">int</span>    <span class="string">`json:&quot;age&quot; db:&quot;user_age&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>反射可以读这些标签。</p><h3 id="10-1-读取标签示例">10.1 读取标签示例</h3><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span> <span class="string">`json:&quot;name&quot; db:&quot;user_name&quot;`</span></span><br><span class="line">    Age  <span class="type">int</span>    <span class="string">`json:&quot;age&quot; db:&quot;user_age&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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(User&#123;&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; t.NumField(); i++ &#123;</span><br><span class="line">        field := t.Field(i)</span><br><span class="line">        fmt.Println(<span class="string">&quot;字段:&quot;</span>, field.Name)</span><br><span class="line">        fmt.Println(<span class="string">&quot;json 标签:&quot;</span>, field.Tag.Get(<span class="string">&quot;json&quot;</span>))</span><br><span class="line">        fmt.Println(<span class="string">&quot;db 标签:&quot;</span>, field.Tag.Get(<span class="string">&quot;db&quot;</span>))</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-2-为什么这很重要">10.2 为什么这很重要</h3><p>像这些能力背后大多依赖反射：</p><ul><li><code>encoding/json</code> 根据 <code>json</code> 标签做编解码</li><li>ORM 根据 <code>db</code> 标签做字段映射</li><li>参数校验库根据 <code>validate</code> 标签做规则验证</li></ul><p>所以学反射，不只是为了自己手写反射代码，更重要的是：</p><p><strong>你开始能读懂这些库为什么能“自动工作”。</strong></p><hr><h2 id="11-修改结构体字段：必须满足条件">11. 修改结构体字段：必须满足条件</h2><p>修改结构体字段是反射里第二个常见操作，但限制也更多。</p><h3 id="11-1-正确示例">11.1 正确示例</h3><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</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">    u := User&#123;Name: <span class="string">&quot;张三&quot;</span>, Age: <span class="number">18</span>&#125;</span><br><span class="line"></span><br><span class="line">    v := reflect.ValueOf(&amp;u).Elem()</span><br><span class="line">    nameField := v.FieldByName(<span class="string">&quot;Name&quot;</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Println(nameField.CanSet()) <span class="comment">// true</span></span><br><span class="line">    nameField.SetString(<span class="string">&quot;李四&quot;</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Println(u.Name) <span class="comment">// 李四</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-2-为什么一定要-u">11.2 为什么一定要 <code>&amp;u</code></h3><p>因为：</p><ul><li><code>reflect.ValueOf(u)</code> 拿到的是值副本</li><li><code>reflect.ValueOf(&amp;u).Elem()</code> 才是原结构体本身</li></ul><h3 id="11-3-必须是导出字段">11.3 必须是导出字段</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    name <span class="type">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么即使你拿到了结构体值，反射也不能随便改这个未导出字段。</p><p>这也是 Go 可见性规则在反射层面的延续。</p><hr><h2 id="12-一个完整实战：打印结构体信息">12. 一个完整实战：打印结构体信息</h2><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span> <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">    Age  <span class="type">int</span>    <span class="string">`json:&quot;age&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">InspectStruct</span><span class="params">(x any)</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(x)</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> t.Kind() == reflect.Ptr &#123;</span><br><span class="line">        t = t.Elem()</span><br><span class="line">        v = v.Elem()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> t.Kind() != reflect.Struct &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;传入的不是结构体&quot;</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">    fmt.Println(<span class="string">&quot;结构体类型:&quot;</span>, t.Name())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; t.NumField(); i++ &#123;</span><br><span class="line">        fieldType := t.Field(i)</span><br><span class="line">        fieldValue := v.Field(i)</span><br><span class="line"></span><br><span class="line">        fmt.Printf(<span class="string">&quot;字段: %s, 类型: %s, 值: %v, json标签: %q\n&quot;</span>,</span><br><span class="line">            fieldType.Name,</span><br><span class="line">            fieldType.Type,</span><br><span class="line">            fieldValue.Interface(),</span><br><span class="line">            fieldType.Tag.Get(<span class="string">&quot;json&quot;</span>),</span><br><span class="line">        )</span><br><span class="line">    &#125;</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">    u := User&#123;Name: <span class="string">&quot;赵六&quot;</span>, Age: <span class="number">28</span>&#125;</span><br><span class="line">    InspectStruct(u)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-1-这个例子体现了哪些关键点">12.1 这个例子体现了哪些关键点</h3><p>它把这节课的几个主干串起来了：</p><ul><li>用 <code>TypeOf</code> / <code>ValueOf</code> 拿类型和值</li><li>用 <code>Kind()</code> 判断是不是指针、是不是结构体</li><li>用 <code>Elem()</code> 解引用</li><li>用 <code>NumField()</code> 和 <code>Field(i)</code> 遍历字段</li><li>用 <code>Tag.Get()</code> 读取标签</li><li>用 <code>Interface()</code> 拿到可打印的普通值</li></ul><p>如果你能把这个函数读顺，说明你已经真正入门反射了。</p><hr><h2 id="13-Interface-是把反射值还原回普通值">13. <code>Interface()</code> 是把反射值还原回普通值</h2><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></pre></td><td class="code"><pre><span class="line">v := reflect.ValueOf(<span class="number">123</span>)</span><br><span class="line">fmt.Println(v.Interface()) <span class="comment">// 123</span></span><br></pre></td></tr></table></figure><p>它的作用可以理解为：</p><blockquote><p>把 <code>reflect.Value</code> 里的内容重新取出来，作为一个普通的 <code>interface&#123;&#125;</code> 值返回</p></blockquote><p>这样你就能：</p><ul><li>交给 <code>fmt.Println</code></li><li>做类型断言</li><li>传给其他普通函数</li></ul><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></pre></td><td class="code"><pre><span class="line">v := reflect.ValueOf(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">value := v.Interface()</span><br><span class="line"></span><br><span class="line">s, ok := value.(<span class="type">string</span>)</span><br><span class="line">fmt.Println(s, ok) <span class="comment">// hello true</span></span><br></pre></td></tr></table></figure><p>所以很多反射流程最后都会回到：</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">fieldValue.Interface()</span><br></pre></td></tr></table></figure><hr><h2 id="14-反射和接口的关系">14. 反射和接口的关系</h2><p>这部分必须讲清楚，否则很多反射代码会看得一头雾水。</p><h3 id="14-1-反射常常从-any-开始">14.1 反射常常从 <code>any</code> 开始</h3><p>很多通用函数写成：</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="function"><span class="keyword">func</span> <span class="title">Inspect</span><span class="params">(x any)</span></span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>为什么？</p><p>因为调用者可能传：</p><ul><li><code>int</code></li><li><code>string</code></li><li><code>User</code></li><li><code>*User</code></li></ul><p>而 <code>any</code> 可以接住所有类型。</p><h3 id="14-2-反射的本质，是把接口里的动态类型拿出来">14.2 反射的本质，是把接口里的动态类型拿出来</h3><p>当一个值被放进接口后，运行时其实仍然记得：</p><ul><li>它的真实类型是什么</li><li>它的真实值是什么</li></ul><p><code>reflect.TypeOf(x)</code> 和 <code>reflect.ValueOf(x)</code> 就是在把这些信息重新拿出来。</p><p>所以你可以把反射理解成：</p><blockquote><p>“对接口中真实类型信息的运行时访问能力”</p></blockquote><p>这个理解非常重要。</p><hr><h2 id="15-反射能做什么，真实项目里为什么常见">15. 反射能做什么，真实项目里为什么常见</h2><p>很多人第一次学反射会觉得：“这玩意我平时好像不用啊。”</p><p>其实你经常在“间接使用”它。</p><h3 id="15-1-编解码">15.1 编解码</h3><p>例如 <code>encoding/json</code>：</p><ul><li>看结构体字段</li><li>读 <code>json</code> 标签</li><li>把 JSON 数据填进对应字段</li></ul><h3 id="15-2-ORM-数据库映射">15.2 ORM / 数据库映射</h3><p>例如很多 ORM 会：</p><ul><li>看结构体字段名</li><li>看数据库标签</li><li>自动构造扫描和映射逻辑</li></ul><h3 id="15-3-配置加载">15.3 配置加载</h3><p>有些配置库会：</p><ul><li>读取环境变量或配置文件</li><li>根据标签把值填充到结构体</li></ul><h3 id="15-4-参数校验">15.4 参数校验</h3><p>有些校验库会：</p><ul><li>遍历结构体字段</li><li>读取 <code>validate:&quot;required&quot;</code> 之类的标签</li><li>按规则做校验</li></ul><h3 id="15-5-框架能力">15.5 框架能力</h3><p>很多框架的“自动注册”“自动绑定”“自动注入”背后，本质都是反射。</p><p>所以反射是一个“底层基础设施工具”。</p><p>业务代码里你可能不常直接写它，但你会大量遇到它的结果。</p><hr><h2 id="16-为什么说反射要慎用">16. 为什么说反射要慎用</h2><p>这节是整课里最重要的价值判断部分。</p><h3 id="16-1-可读性差">16.1 可读性差</h3><p>普通代码一眼就知道在干什么：</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">user.Name = <span class="string">&quot;张三&quot;</span></span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">v := reflect.ValueOf(&amp;user).Elem()</span><br><span class="line">f := v.FieldByName(<span class="string">&quot;Name&quot;</span>)</span><br><span class="line">f.SetString(<span class="string">&quot;张三&quot;</span>)</span><br></pre></td></tr></table></figure><p>显然更绕。</p><h3 id="16-2-容易-panic">16.2 容易 panic</h3><p>例如：</p><ul><li>对 <code>string</code> 调 <code>Int()</code></li><li>对不可设置值调用 <code>Set</code></li><li>对非指针调用 <code>Elem()</code></li><li>字段不存在却继续操作</li></ul><p>这些都很容易在运行时直接 panic。</p><h3 id="16-3-性能更差">16.3 性能更差</h3><p>反射需要：</p><ul><li>做动态类型检查</li><li>做额外包装</li><li>走运行时分派逻辑</li></ul><p>这通常比直接访问字段、直接调用函数更慢。</p><h3 id="16-4-编译器帮不了太多">16.4 编译器帮不了太多</h3><p>普通代码很多错误能在编译期发现。</p><p>反射代码则有一大类错误只能等到运行时暴露。</p><p>所以反射的原则很简单：</p><blockquote><p>能不用就不用，必须用时把范围收窄。</p></blockquote><hr><h2 id="17-泛型出现后，哪些场景不必再用反射">17. 泛型出现后，哪些场景不必再用反射</h2><p>这是你现在这个学习阶段很值得建立的判断力。</p><p>在 Go 引入泛型之前，很多“通用处理”只能在：</p><ul><li>重复代码</li><li>反射</li><li><code>interface&#123;&#125;</code> + 类型断言</li></ul><p>之间选。</p><p>现在多了一个更好的选项：泛型。</p><h3 id="17-1-例如切片工具函数">17.1 例如切片工具函数</h3><p>过去有人会写各种“反射版通用切片处理”。</p><p>现在像这种场景：</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="function"><span class="keyword">func</span> <span class="title">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span></span><br></pre></td></tr></table></figure><p>显然泛型比反射更合适。</p><h3 id="17-2-那反射还剩什么价值">17.2 那反射还剩什么价值</h3><p>反射仍然适合：</p><ul><li>处理结构体标签</li><li>处理运行时未知结构</li><li>写通用框架和基础库</li><li>编解码、绑定、注入、校验这类动态机制</li></ul><p>所以一个实用判断是：</p><ul><li>“同一算法适用于多种已知类型” -&gt; 更像泛型</li><li>“运行时才知道结构和字段” -&gt; 更像反射</li></ul><hr><h2 id="18-常见坑总结">18. 常见坑总结</h2><h3 id="18-1-忘了判断-Kind">18.1 忘了判断 <code>Kind</code></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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">PrintInt</span><span class="params">(x any)</span></span> &#123;</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line">    fmt.Println(v.Int())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果传进来不是整数，就会 panic。</p><p>正确思路：</p><ul><li>先判断 <code>v.Kind()</code></li><li>再做对应操作</li></ul><h3 id="18-2-对非指针调用-Elem">18.2 对非指针调用 <code>Elem()</code></h3><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">v := reflect.ValueOf(<span class="number">10</span>)</span><br><span class="line">fmt.Println(v.Elem()) <span class="comment">// panic</span></span><br></pre></td></tr></table></figure><p><code>Elem()</code> 不是随便都能调，至少得先确认它是指针或接口等可解开的值。</p><h3 id="18-3-以为拿到-Value-就一定能改">18.3 以为拿到 <code>Value</code> 就一定能改</h3><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">v := reflect.ValueOf(<span class="number">10</span>)</span><br><span class="line">fmt.Println(v.CanSet()) <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>只有可寻址、可设置的值才能改，通常需要：</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">reflect.ValueOf(&amp;x).Elem()</span><br></pre></td></tr></table></figure><h3 id="18-4-忘了字段可能不存在">18.4 忘了字段可能不存在</h3><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">f := v.FieldByName(<span class="string">&quot;Email&quot;</span>)</span><br><span class="line">f.SetString(<span class="string">&quot;a@b.com&quot;</span>) <span class="comment">// 可能 panic</span></span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> !f.IsValid() &#123;</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="18-5-忘了字段可能不可设置">18.5 忘了字段可能不可设置</h3><p>即使字段存在，也不代表可以改。</p><p>要先看：</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">f.CanSet()</span><br></pre></td></tr></table></figure><h3 id="18-6-在普通业务逻辑里滥用反射">18.6 在普通业务逻辑里滥用反射</h3><p>例如你明明知道是：</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">user.Name</span><br></pre></td></tr></table></figure><p>却还写成：</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">reflect.ValueOf(&amp;user).Elem().FieldByName(<span class="string">&quot;Name&quot;</span>)</span><br></pre></td></tr></table></figure><p>这基本只会让代码更差。</p><h3 id="18-7-反射代码不做错误保护">18.7 反射代码不做错误保护</h3><p>反射比普通代码更应该：</p><ul><li>提前判断类型</li><li>提前判断字段是否存在</li><li>提前判断是否可设置</li><li>在必要处返回错误而不是直接 panic</li></ul><hr><h2 id="19-一个更实用的安全版本：按字段名设置字符串">19. 一个更实用的安全版本：按字段名设置字符串</h2><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></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;reflect&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    City <span class="type">string</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">SetStringField</span><span class="params">(ptr any, fieldName <span class="type">string</span>, newValue <span class="type">string</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    v := reflect.ValueOf(ptr)</span><br><span class="line">    <span class="keyword">if</span> v.Kind() != reflect.Ptr &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要传入结构体指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    elem := v.Elem()</span><br><span class="line">    <span class="keyword">if</span> elem.Kind() != reflect.Struct &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要传入指向结构体的指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    field := elem.FieldByName(fieldName)</span><br><span class="line">    <span class="keyword">if</span> !field.IsValid() &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 %s 不存在&quot;</span>, fieldName)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> field.Kind() != reflect.String &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 %s 不是 string 类型&quot;</span>, fieldName)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !field.CanSet() &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 %s 不可设置&quot;</span>, fieldName)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    field.SetString(newValue)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</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">    u := User&#123;Name: <span class="string">&quot;张三&quot;</span>, City: <span class="string">&quot;北京&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    err := SetStringField(&amp;u, <span class="string">&quot;City&quot;</span>, <span class="string">&quot;上海&quot;</span>)</span><br><span class="line">    fmt.Println(err) <span class="comment">// &lt;nil&gt;</span></span><br><span class="line">    fmt.Println(u)   <span class="comment">// &#123;张三 上海&#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="19-1-这个例子为什么比直接-panic-更好">19.1 这个例子为什么比直接 panic 更好</h3><p>因为它在每一步都做了边界检查：</p><ul><li>是不是指针</li><li>指向的是不是结构体</li><li>字段存不存在</li><li>字段类型对不对</li><li>字段能不能改</li></ul><p>这就是反射代码和普通代码最大的写法差别：</p><p><strong>你必须更防御式。</strong></p><hr><h2 id="20-本课练习">20. 本课练习</h2><h3 id="练习-1：打印变量类型与值">练习 1：打印变量类型与值</h3><p>写一个函数：</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="function"><span class="keyword">func</span> <span class="title">Describe</span><span class="params">(x any)</span></span></span><br></pre></td></tr></table></figure><p>要求打印：</p><ul><li>具体类型</li><li><code>Kind</code></li><li>当前值</li></ul><p>并分别测试：</p><ul><li><code>int</code></li><li><code>string</code></li><li><code>[]int</code></li><li>结构体</li></ul><hr><h3 id="练习-2：遍历结构体字段">练习 2：遍历结构体字段</h3><p>定义一个 <code>Book</code> 结构体，包含：</p><ul><li><code>Title</code></li><li><code>Author</code></li><li><code>Price</code></li></ul><p>写一个函数，使用反射打印每个字段的：</p><ul><li>字段名</li><li>字段类型</li><li>字段值</li></ul><hr><h3 id="练习-3：读取标签">练习 3：读取标签</h3><p>给 <code>Book</code> 增加标签：</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">type</span> Book <span class="keyword">struct</span> &#123;</span><br><span class="line">    Title  <span class="type">string</span>  <span class="string">`json:&quot;title&quot; db:&quot;title&quot;`</span></span><br><span class="line">    Author <span class="type">string</span>  <span class="string">`json:&quot;author&quot; db:&quot;author&quot;`</span></span><br><span class="line">    Price  <span class="type">float64</span> <span class="string">`json:&quot;price&quot; db:&quot;price&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写一个函数读取并打印每个字段的 <code>json</code> 和 <code>db</code> 标签。</p><hr><h3 id="练习-4：修改字段值">练习 4：修改字段值</h3><p>写一个函数：</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="function"><span class="keyword">func</span> <span class="title">SetIntField</span><span class="params">(ptr any, fieldName <span class="type">string</span>, value <span class="type">int64</span>)</span></span> <span class="type">error</span></span><br></pre></td></tr></table></figure><p>要求：</p><ul><li>只允许修改 <code>int</code> 类型字段</li><li>如果字段不存在、不是 <code>int</code>、不可设置，都返回错误</li></ul><hr><h3 id="练习-5：对比反射与普通写法">练习 5：对比反射与普通写法</h3><p>分别实现下面两种逻辑：</p><ol><li>直接把 <code>user.Name</code> 改成 <code>&quot;李四&quot;</code></li><li>用反射把字段 <code>&quot;Name&quot;</code> 改成 <code>&quot;李四&quot;</code></li></ol><p>然后比较：</p><ul><li>哪个更简单</li><li>哪个更安全</li><li>哪个更适合普通业务代码</li></ul><hr><h2 id="21-自测题">21. 自测题</h2><h3 id="21-1-概念题">21.1 概念题</h3><ol><li>Go 反射主要解决什么问题？</li><li><code>reflect.Type</code> 和 <code>reflect.Value</code> 分别表示什么？</li><li><code>Type</code> 和 <code>Kind</code> 有什么区别？</li><li>为什么很多反射代码都要先判断 <code>Kind()</code>？</li><li><code>Elem()</code> 在什么场景下最常见？它的作用是什么？</li><li>为什么通过反射修改值时通常要传指针？</li><li><code>CanSet()</code> 的意义是什么？</li><li>为什么说反射更适合框架和基础库，而不适合滥用在普通业务代码里？</li></ol><h3 id="21-2-代码阅读题">21.2 代码阅读题</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetName</span><span class="params">(x any)</span></span> &#123;</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line">    field := v.FieldByName(<span class="string">&quot;Name&quot;</span>)</span><br><span class="line">    field.SetString(<span class="string">&quot;李四&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p><strong>问题 1：没有传指针，也没有 <code>Elem()</code>。</strong></p><ul><li><code>reflect.ValueOf(x)</code> 如果拿到的是结构体值副本，字段通常不可设置</li><li>正确做法通常是传入结构体指针，然后 <code>Elem()</code> 取到原结构体</li></ul><p><strong>问题 2：没有做任何安全检查。</strong></p><ul><li>没判断 <code>x</code> 是不是结构体或结构体指针</li><li>没判断字段 <code>&quot;Name&quot;</code> 是否存在</li><li>没判断字段是不是 <code>string</code></li><li>没判断字段是否可设置</li></ul><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="function"><span class="keyword">func</span> <span class="title">SetName</span><span class="params">(x any)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    v := reflect.ValueOf(x)</span><br><span class="line">    <span class="keyword">if</span> v.Kind() != reflect.Ptr &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要传入指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    elem := v.Elem()</span><br><span class="line">    <span class="keyword">if</span> elem.Kind() != reflect.Struct &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;需要传入结构体指针&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    field := elem.FieldByName(<span class="string">&quot;Name&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !field.IsValid() &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 Name 不存在&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> field.Kind() != reflect.String &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 Name 不是 string 类型&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> !field.CanSet() &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;字段 Name 不可设置&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    field.SetString(<span class="string">&quot;李四&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></details><hr><h2 id="22-本课总结">22. 本课总结</h2><p>这一课你已经进入 Go 里最“动态”的能力之一。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>反射目标</td><td>在运行时查看和操作类型与值</td></tr><tr><td>两大核心</td><td><code>reflect.Type</code> 看类型，<code>reflect.Value</code> 看值</td></tr><tr><td>关键判断</td><td><code>Kind()</code> 决定当前值该怎么处理</td></tr><tr><td>关键入口</td><td><code>Elem()</code> 用来进入指针或接口内部值</td></tr><tr><td>修改前提</td><td>指针、正确目标、<code>CanSet()</code> 为真</td></tr><tr><td>典型用途</td><td>标签读取、编解码、框架能力、动态绑定</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>反射解决的是“运行时才知道结构”的问题</strong></li><li><strong>反射代码一定要先做类型和边界检查</strong></li><li><strong>很多框架大量使用反射，但普通业务代码不该为了炫技滥用它</strong></li><li><strong>泛型解决的是通用类型复用，反射解决的是运行时动态检查，两者不是一回事</strong></li></ol><hr><h2 id="23-下一课预告">23. 下一课预告</h2><p>你已经学完泛型和反射，接下来我们要转向另一个非常重要的高级主题：Go 的内存行为。</p><p>下一课：<strong>内存与逃逸分析基础</strong></p><p>会重点讲：</p><ul><li>栈和堆的基本区别</li><li>什么是逃逸分析</li><li>哪些写法容易导致变量逃逸到堆上</li><li>值传递、指针传递和内存分配之间的关系</li><li>怎么建立对性能和内存行为的初步判断</li></ul><p>学完下一课，你会开始真正理解“为什么这段 Go 代码会有这样的性能表现”。</p>]]></content>
    
    
    <summary type="html">Go语言系列35</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 34 课：泛型入门</title>
    <link href="https://yjyrichard.github.io/posts/35fca0e4.html"/>
    <id>https://yjyrichard.github.io/posts/35fca0e4.html</id>
    <published>2026-03-22T15:28:11.755Z</published>
    <updated>2026-03-22T15:40:40.037Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 34 课：泛型入门</h1><blockquote><p>学习定位：这是整套 Go 教程的第 34 课，也是阶段六（高级进阶阶段）的第一课。<br>前置要求：已经完成第 33 课，理解 Go 的项目结构、包组织、接口、错误处理与测试基础。<br>本课目标：理解 Go 泛型要解决的核心问题，掌握类型参数、约束、泛型函数和泛型结构体的基本写法，学会 <code>any</code>、<code>comparable</code>、类型集合和 <code>~</code> 的用法，并知道泛型适合解决什么问题、不适合解决什么问题。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>你前面已经写过很多“长得很像”的函数了。</p><p>比如：</p><ul><li>求两个 <code>int</code> 的较大值</li><li>求两个 <code>float64</code> 的较大值</li><li>判断一个 <code>[]int</code> 里有没有某个元素</li><li>判断一个 <code>[]string</code> 里有没有某个元素</li><li>写一个整数栈</li><li>又想写一个字符串栈</li></ul><p>如果没有泛型，你通常只有三种办法：</p><ol><li>为每种类型各写一份</li><li>用 <code>interface&#123;&#125;</code> 接收一切，再做类型断言</li><li>硬写死只支持一种类型</li></ol><p>这三种办法都不够理想：</p><ul><li>重复写多份代码，维护成本高</li><li><code>interface&#123;&#125;</code> 丢失类型信息，不安全，还容易出错</li><li>写死类型后，可复用性又很差</li></ul><p><strong>泛型的目标就是：让你把“算法或数据结构的通用部分”抽出来，同时保留类型安全。</strong></p><p>也就是说：</p><ul><li>你不用为 <code>int</code>、<code>float64</code>、<code>string</code> 各写一份同样逻辑</li><li>调用者在编译期就能得到类型检查</li><li>不需要到处做类型断言</li></ul><p>这一课的重点就是把这个能力讲清楚。</p><hr><h2 id="2-先理解：为什么-Go-很晚才加入泛型">2. 先理解：为什么 Go 很晚才加入泛型</h2><p>很多语言很早就有泛型，为什么 Go 到后面才加？</p><p>因为 Go 的设计哲学一直比较克制：</p><ul><li>优先简单</li><li>优先可读</li><li>优先少而稳的语言特性</li></ul><p>Go 团队不是不知道泛型有用，而是担心两个问题：</p><ol><li>语法太复杂，破坏 Go 一贯的简洁性</li><li>滥用泛型后，代码会变得抽象、绕、难读</li></ol><p>所以 Go 直到 1.18 才正式加入泛型，而且设计得相对克制。</p><p>这也决定了一个非常重要的使用原则：</p><blockquote><p>Go 泛型不是让你把所有代码都改成“高度抽象模板”，而是让你在真正需要复用通用逻辑时，有一个比 <code>interface&#123;&#125;</code> 更安全的工具。</p></blockquote><p>你学泛型时，必须一直记着这个原则。</p><hr><h2 id="3-没有泛型时，问题到底出在哪">3. 没有泛型时，问题到底出在哪</h2><h3 id="3-1-重复代码">3.1 重复代码</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><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">MaxInt</span><span class="params">(a, b <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</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">MaxFloat</span><span class="params">(a, b <span class="type">float64</span>)</span></span> <span class="type">float64</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你会发现逻辑完全一样，只有类型不同。</p><p>如果以后再来：</p><ul><li><code>int64</code></li><li><code>uint</code></li><li><code>string</code>（按字典序比较）</li></ul><p>你还得继续复制。</p><h3 id="3-2-interface-虽然通用，但不安全">3.2 <code>interface&#123;&#125;</code> 虽然通用，但不安全</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Contains</span><span class="params">(items []<span class="keyword">interface</span>&#123;&#125;, target <span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看起来很通用，实际上问题很多：</p><ul><li>调用者必须把数据都装箱成 <code>interface&#123;&#125;</code></li><li>容易混入错误类型</li><li>编译器很难帮你检查</li><li>代码可读性差</li></ul><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">items := []<span class="keyword">interface</span>&#123;&#125;&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">Contains(items, <span class="string">&quot;2&quot;</span>)</span><br></pre></td></tr></table></figure><p>代码能编译，但逻辑根本没意义。</p><h3 id="3-3-泛型的价值就在这里">3.3 泛型的价值就在这里</h3><p>如果你想表达的是：</p><ul><li>“这是一段对很多类型都适用的逻辑”</li><li>“但调用时必须保持类型一致”</li></ul><p>那泛型就非常合适。</p><hr><h2 id="4-泛型最核心的两个概念">4. 泛型最核心的两个概念</h2><p>学泛型，只要先吃透两个词：</p><ol><li><strong>类型参数</strong></li><li><strong>约束</strong></li></ol><h3 id="4-1-类型参数是什么">4.1 类型参数是什么</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">PrintValue</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(v T)</span></span> &#123;</span><br><span class="line">    fmt.Println(v)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>[T any]</code> 就是类型参数列表。</p><p>可以把它理解为：</p><ul><li><code>T</code> 是一个“类型变量”</li><li>具体是什么类型，调用时再决定</li><li><code>any</code> 表示它目前没有额外限制</li></ul><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></pre></td><td class="code"><pre><span class="line">PrintValue(<span class="number">10</span>)        <span class="comment">// T 是 int</span></span><br><span class="line">PrintValue(<span class="string">&quot;hello&quot;</span>)   <span class="comment">// T 是 string</span></span><br><span class="line">PrintValue(<span class="literal">true</span>)      <span class="comment">// T 是 bool</span></span><br></pre></td></tr></table></figure><h3 id="4-2-约束是什么">4.2 约束是什么</h3><p>约束就是告诉编译器：</p><blockquote><p>这个类型参数不是什么都能来，它必须满足某些条件。</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Equal</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(a, b T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a == b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>comparable</code> 就是约束，意思是：</p><ul><li><code>T</code> 必须是可比较的类型</li><li>否则就不能用 <code>==</code></li></ul><p>如果没有这个约束，编译器就不能保证 <code>a == b</code> 一定合法。</p><p>所以你可以先把泛型读成一句人话：</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="function"><span class="keyword">func</span> <span class="title">Equal</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(a, b T)</span></span> <span class="type">bool</span></span><br></pre></td></tr></table></figure><p>等于：</p><blockquote><p>定义一个函数 <code>Equal</code>，它适用于任意可比较类型 <code>T</code>。</p></blockquote><p>这就是泛型最核心的阅读方式。</p><hr><h2 id="5-第一个真正有意义的泛型函数">5. 第一个真正有意义的泛型函数</h2><h3 id="5-1-写一个通用的-Contains">5.1 写一个通用的 Contains</h3><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 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">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</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">    nums := []<span class="type">int</span>&#123;<span class="number">10</span>, <span class="number">20</span>, <span class="number">30</span>&#125;</span><br><span class="line">    fmt.Println(Contains(nums, <span class="number">20</span>)) <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line">    names := []<span class="type">string</span>&#123;<span class="string">&quot;张三&quot;</span>, <span class="string">&quot;李四&quot;</span>, <span class="string">&quot;王五&quot;</span>&#125;</span><br><span class="line">    fmt.Println(Contains(names, <span class="string">&quot;赵六&quot;</span>)) <span class="comment">// false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-这段代码为什么比-interface-好">5.2 这段代码为什么比 <code>interface&#123;&#125;</code> 好</h3><p>因为它同时满足了三件事：</p><ol><li><strong>一套代码复用多个类型</strong></li><li><strong>调用时类型必须一致</strong></li><li><strong>编译器能提前发现错误</strong></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></pre></td><td class="code"><pre><span class="line">nums := []<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">Contains(nums, <span class="string">&quot;2&quot;</span>)</span><br></pre></td></tr></table></figure><p>这段代码会直接编译失败，因为：</p><ul><li><code>items</code> 是 <code>[]int</code></li><li><code>target</code> 却是 <code>string</code></li></ul><p>这就是泛型相比 <code>interface&#123;&#125;</code> 最大的工程价值：<strong>通用，但仍然有强类型约束。</strong></p><hr><h2 id="6-类型推断：很多时候不用手动写类型">6. 类型推断：很多时候不用手动写类型</h2><p>刚看到泛型时，很多人会以为每次都要这样调用：</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">Contains[<span class="type">int</span>]([]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;, <span class="number">2</span>)</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">Contains([]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;, <span class="number">2</span>)</span><br><span class="line">Contains([]<span class="type">string</span>&#123;<span class="string">&quot;a&quot;</span>, <span class="string">&quot;b&quot;</span>&#125;, <span class="string">&quot;a&quot;</span>)</span><br></pre></td></tr></table></figure><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">var</span> nums []<span class="type">int</span></span><br><span class="line">_ = Contains[<span class="type">int</span>](nums, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><p>实战里你会发现：</p><ul><li><strong>定义泛型时要会写类型参数</strong></li><li><strong>调用泛型时多数情况不用手写</strong></li></ul><p>所以不要把泛型想得太重。</p><hr><h2 id="7-any-到底是什么">7. <code>any</code> 到底是什么</h2><h3 id="7-1-any-本质上是-interface">7.1 <code>any</code> 本质上是 <code>interface&#123;&#125;</code></h3><p>Go 里：</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">any</span><br></pre></td></tr></table></figure><p>只是：</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="keyword">interface</span>&#123;&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">PrintValue</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(v T)</span></span> &#123;</span><br><span class="line">    fmt.Println(v)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>意思不是“运行时随便什么都行”，而是：</p><blockquote><p>这个类型参数当前没有额外约束。</p></blockquote><h3 id="7-2-any-不代表你能对-T-做任何操作">7.2 <code>any</code> 不代表你能对 T 做任何操作</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">AddOne</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(v T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">return</span> v + <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面这段代码是错的。</p><p>因为虽然 <code>T</code> 可以是任意类型，但并不是任意类型都支持 <code>+ 1</code>。</p><p>所以：</p><ul><li><code>any</code> 表示“接收范围广”</li><li>不表示“可做任意操作”</li></ul><p>你能对 <code>T</code> 做什么，取决于它的约束允许什么。</p><hr><h2 id="8-comparable-是最常用的入门约束">8. <code>comparable</code> 是最常用的入门约束</h2><h3 id="8-1-它表示“可用-和-比较”">8.1 它表示“可用 == 和 != 比较”</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IsSame</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(a, b T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a == b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这时 <code>T</code> 可以是：</p><ul><li>整数</li><li>浮点数</li><li>字符串</li><li>布尔值</li><li>指针</li><li>可比较的结构体</li><li>元素可比较的数组</li></ul><h3 id="8-2-哪些类型不满足-comparable">8.2 哪些类型不满足 <code>comparable</code></h3><p>比如：</p><ul><li><code>slice</code></li><li><code>map</code></li><li><code>func</code></li></ul><p>这些都不能直接用 <code>==</code> 比较。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IsSame</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(a, b T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a == b</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">    a := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line">    b := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 编译错误：[]int 不满足 comparable</span></span><br><span class="line">    fmt.Println(IsSame(a, b))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是约束在编译期保护你的地方。</p><h3 id="8-3-一个很实用的泛型-Set">8.3 一个很实用的泛型 Set</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Set[T comparable] <span class="keyword">map</span>[T]<span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewSet</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">()</span></span> Set[T] &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">make</span>(Set[T])</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">(s Set[T])</span></span> Add(v T) &#123;</span><br><span class="line">    s[v] = <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</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">(s Set[T])</span></span> Has(v T) <span class="type">bool</span> &#123;</span><br><span class="line">    _, ok := s[v]</span><br><span class="line">    <span class="keyword">return</span> ok</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里必须使用 <code>comparable</code>，因为 map 的键本身就要求可比较。</p><hr><h2 id="9-自定义约束：让泛型真正可控">9. 自定义约束：让泛型真正可控</h2><p>如果 <code>any</code> 太宽、<code>comparable</code> 又不够，你就需要自定义约束。</p><h3 id="9-1-最简单的自定义约束">9.1 最简单的自定义约束</h3><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">type</span> Number <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</span> | ~<span class="type">int8</span> | ~<span class="type">int16</span> | ~<span class="type">int32</span> | ~<span class="type">int64</span> |</span><br><span class="line">        ~<span class="type">uint</span> | ~<span class="type">uint8</span> | ~<span class="type">uint16</span> | ~<span class="type">uint32</span> | ~<span class="type">uint64</span> | ~<span class="type">uintptr</span> |</span><br><span class="line">        ~<span class="type">float32</span> | ~<span class="type">float64</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个约束表示：</p><ul><li><code>T</code> 必须是这些底层类型之一</li><li>这样你就可以安全使用 <code>+</code>、<code>-</code>、<code>*</code>、<code>/</code></li></ul><h3 id="9-2-用它写一个求和函数">9.2 用它写一个求和函数</h3><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="function"><span class="keyword">func</span> <span class="title">Sum</span>[<span class="title">T</span> <span class="title">Number</span>]<span class="params">(items []T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">var</span> total T</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        total += item</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> total</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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">fmt.Println(Sum([]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;))           <span class="comment">// 6</span></span><br><span class="line">fmt.Println(Sum([]<span class="type">float64</span>&#123;<span class="number">1.5</span>, <span class="number">2.5</span>, <span class="number">3.0</span>&#125;)) <span class="comment">// 7</span></span><br></pre></td></tr></table></figure><h3 id="9-3-为什么不直接用-any">9.3 为什么不直接用 <code>any</code></h3><p>因为如果写成：</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="function"><span class="keyword">func</span> <span class="title">Sum</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(items []T)</span></span> T</span><br></pre></td></tr></table></figure><p>编译器根本不知道：</p><ul><li><code>T</code> 能不能做加法</li><li><code>T</code> 有没有零值可用于累加</li></ul><p>所以泛型不是“偷懒少写类型”，而是“把可用类型的边界写清楚”。</p><hr><h2 id="10-是什么意思">10. <code>~</code> 是什么意思</h2><p>这是泛型里一个很重要、也很容易一开始看懵的符号。</p><h3 id="10-1-int-的含义">10.1 <code>~int</code> 的含义</h3><p><code>~int</code> 表示：</p><blockquote><p>底层类型是 <code>int</code> 的所有类型</p></blockquote><p>例如：</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="keyword">type</span> MyInt <span class="type">int</span></span><br></pre></td></tr></table></figure><p><code>MyInt</code> 的底层类型就是 <code>int</code>，所以它满足 <code>~int</code>。</p><h3 id="10-2-为什么需要">10.2 为什么需要 <code>~</code></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> IntOnly <span class="keyword">interface</span> &#123;</span><br><span class="line">    <span class="type">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那它只接受真正的 <code>int</code>，不接受：</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="keyword">type</span> MyInt <span class="type">int</span></span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> IntLike <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么 <code>int</code> 和 <code>MyInt</code> 都可以。</p><h3 id="10-3-一个例子看区别">10.3 一个例子看区别</h3><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></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="keyword">type</span> MyInt <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> IntLike <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</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">Double</span>[<span class="title">T</span> <span class="title">IntLike</span>]<span class="params">(v T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">return</span> v * <span class="number">2</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">var</span> a <span class="type">int</span> = <span class="number">10</span></span><br><span class="line">    <span class="keyword">var</span> b MyInt = <span class="number">20</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(Double(a)) <span class="comment">// 20</span></span><br><span class="line">    fmt.Println(Double(b)) <span class="comment">// 40</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果没有 <code>~</code>，第二个调用就不一定能通过。</p><h3 id="10-4-实际理解方式">10.4 实际理解方式</h3><p>初学时你可以把 <code>~</code> 记成：</p><blockquote><p>“允许底层类型是某种类型的自定义类型也参与进来”</p></blockquote><p>这就够用了。</p><hr><h2 id="11-写一个泛型-Max：把约束和操作结合起来">11. 写一个泛型 Max：把约束和操作结合起来</h2><p>下面写一个真正常用的泛型函数：求较大值。</p><h3 id="11-1-先定义有序类型约束">11.1 先定义有序类型约束</h3><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="keyword">type</span> Ordered <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</span> | ~<span class="type">int8</span> | ~<span class="type">int16</span> | ~<span class="type">int32</span> | ~<span class="type">int64</span> |</span><br><span class="line">        ~<span class="type">uint</span> | ~<span class="type">uint8</span> | ~<span class="type">uint16</span> | ~<span class="type">uint32</span> | ~<span class="type">uint64</span> | ~<span class="type">uintptr</span> |</span><br><span class="line">        ~<span class="type">float32</span> | ~<span class="type">float64</span> |</span><br><span class="line">        ~<span class="type">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里把字符串也放进来了，因为字符串可以比较大小。</p><h3 id="11-2-定义泛型函数">11.2 定义泛型函数</h3><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="function"><span class="keyword">func</span> <span class="title">Max</span>[<span class="title">T</span> <span class="title">Ordered</span>]<span class="params">(a, b T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line">fmt.Println(Max(<span class="number">10</span>, <span class="number">20</span>))         <span class="comment">// 20</span></span><br><span class="line">fmt.Println(Max(<span class="number">3.14</span>, <span class="number">2.71</span>))     <span class="comment">// 3.14</span></span><br><span class="line">fmt.Println(Max(<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;zoo&quot;</span>)) <span class="comment">// zoo</span></span><br></pre></td></tr></table></figure><h3 id="11-3-这就是“既通用又受限”">11.3 这就是“既通用又受限”</h3><p>它不是任意类型都支持，而是：</p><ul><li>只支持有序类型</li><li>一旦支持，就可以安全使用 <code>&gt;</code></li></ul><p>这就是泛型设计的核心模式：</p><ol><li>先定义“哪些类型可以来”</li><li>再在这个边界内写通用逻辑</li></ol><hr><h2 id="12-泛型结构体：通用数据结构终于好写了">12. 泛型结构体：通用数据结构终于好写了</h2><p>泛型不仅能修饰函数，也能修饰类型。</p><h3 id="12-1-写一个泛型栈">12.1 写一个泛型栈</h3><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></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="keyword">type</span> Stack[T any] <span class="keyword">struct</span> &#123;</span><br><span class="line">    items []T</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">(s *Stack[T])</span></span> Push(v T) &#123;</span><br><span class="line">    s.items = <span class="built_in">append</span>(s.items, v)</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">(s *Stack[T])</span></span> Pop() (T, <span class="type">bool</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(s.items) == <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">var</span> zero T</span><br><span class="line">        <span class="keyword">return</span> zero, <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    idx := <span class="built_in">len</span>(s.items) - <span class="number">1</span></span><br><span class="line">    value := s.items[idx]</span><br><span class="line">    s.items = s.items[:idx]</span><br><span class="line">    <span class="keyword">return</span> value, <span class="literal">true</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">(s *Stack[T])</span></span> Len() <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">len</span>(s.items)</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">var</span> intStack Stack[<span class="type">int</span>]</span><br><span class="line">    intStack.Push(<span class="number">10</span>)</span><br><span class="line">    intStack.Push(<span class="number">20</span>)</span><br><span class="line">    v, ok := intStack.Pop()</span><br><span class="line">    fmt.Println(v, ok) <span class="comment">// 20 true</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> strStack Stack[<span class="type">string</span>]</span><br><span class="line">    strStack.Push(<span class="string">&quot;Go&quot;</span>)</span><br><span class="line">    strStack.Push(<span class="string">&quot;泛型&quot;</span>)</span><br><span class="line">    s, ok := strStack.Pop()</span><br><span class="line">    fmt.Println(s, ok) <span class="comment">// 泛型 true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-2-var-zero-T-是什么技巧">12.2 <code>var zero T</code> 是什么技巧</h3><p>因为 <code>T</code> 是类型参数，你没法直接写：</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="keyword">return</span> <span class="string">&quot;&quot;</span>, <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>或者：</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="keyword">return</span> <span class="number">0</span>, <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>因为你不知道 <code>T</code> 到底是 <code>string</code> 还是 <code>int</code>。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> zero T</span><br><span class="line"><span class="keyword">return</span> zero, <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>意思是返回 <code>T</code> 的零值。</p><p>这是写泛型容器时非常常见的技巧。</p><hr><h2 id="13-泛型结构体的方法怎么写">13. 泛型结构体的方法怎么写</h2><p>很多人第一次看到这个语法会别扭：</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="function"><span class="keyword">func</span> <span class="params">(s *Stack[T])</span></span> Push(v T)</span><br></pre></td></tr></table></figure><p>你可以把它拆开理解：</p><ul><li><code>Stack[T]</code> 是一个泛型类型</li><li><code>Push</code> 是这个类型的方法</li><li>方法里也可以继续使用 <code>T</code></li></ul><h3 id="13-1-再看一个队列例子">13.1 再看一个队列例子</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Queue[T any] <span class="keyword">struct</span> &#123;</span><br><span class="line">    items []T</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">(q *Queue[T])</span></span> Enqueue(v T) &#123;</span><br><span class="line">    q.items = <span class="built_in">append</span>(q.items, v)</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">(q *Queue[T])</span></span> Dequeue() (T, <span class="type">bool</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(q.items) == <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">var</span> zero T</span><br><span class="line">        <span class="keyword">return</span> zero, <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    value := q.items[<span class="number">0</span>]</span><br><span class="line">    q.items = q.items[<span class="number">1</span>:]</span><br><span class="line">    <span class="keyword">return</span> value, <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> q Queue[<span class="type">string</span>]</span><br><span class="line">q.Enqueue(<span class="string">&quot;A&quot;</span>)</span><br><span class="line">q.Enqueue(<span class="string">&quot;B&quot;</span>)</span><br></pre></td></tr></table></figure><p>本质上和普通结构体方法没什么不同，只是这个结构体多了一个类型参数。</p><hr><h2 id="14-泛型和接口的关系：不是替代，而是互补">14. 泛型和接口的关系：不是替代，而是互补</h2><p>很多人学到泛型后会问：</p><blockquote><p>有了泛型，是不是接口就不重要了？</p></blockquote><p>不是。两者解决的问题不同。</p><h3 id="14-1-接口解决“行为抽象”">14.1 接口解决“行为抽象”</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Reader <span class="keyword">interface</span> &#123;</span><br><span class="line">    Read(p []<span class="type">byte</span>) (<span class="type">int</span>, <span class="type">error</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里抽象的是：</p><ul><li>“谁能读取数据”</li><li>重点是行为一致</li></ul><h3 id="14-2-泛型解决“类型通用”">14.2 泛型解决“类型通用”</h3><p>比如：</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="function"><span class="keyword">func</span> <span class="title">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span></span><br></pre></td></tr></table></figure><p>这里抽象的是：</p><ul><li>“同一种逻辑适用于多种类型”</li><li>重点是算法复用</li></ul><h3 id="14-3-一个非常实用的判断">14.3 一个非常实用的判断</h3><p>如果你在抽象：</p><ul><li>“它能做什么” -&gt; 更像接口</li><li>“它是什么类型，但逻辑相同” -&gt; 更像泛型</li></ul><h3 id="14-4-两者可以一起用">14.4 两者可以一起用</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Stringer <span class="keyword">interface</span> &#123;</span><br><span class="line">    String() <span class="type">string</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">PrintAll</span>[<span class="title">T</span> <span class="title">Stringer</span>]<span class="params">(items []T)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        fmt.Println(item.String())</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就是：</p><ul><li>泛型负责“切片元素类型可变化”</li><li>接口负责“元素必须有 <code>String()</code> 方法”</li></ul><p>这说明泛型和接口不是对立关系，而是可以配合使用。</p><hr><h2 id="15-什么时候适合用泛型">15. 什么时候适合用泛型</h2><p>这节很重要，因为泛型最容易学会语法后乱用。</p><h3 id="15-1-适合场景一：通用数据结构">15.1 适合场景一：通用数据结构</h3><p>例如：</p><ul><li>栈 <code>Stack[T]</code></li><li>队列 <code>Queue[T]</code></li><li>集合 <code>Set[T]</code></li><li>堆、环形缓冲区等</li></ul><p>这些结构的行为逻辑相同，只是元素类型不同，非常适合用泛型。</p><h3 id="15-2-适合场景二：通用算法">15.2 适合场景二：通用算法</h3><p>例如：</p><ul><li><code>Contains</code></li><li><code>Filter</code></li><li><code>Map</code></li><li><code>Reduce</code></li><li><code>Min</code> / <code>Max</code></li><li>查找、排序辅助函数</li></ul><p>前提是：这些算法对多种类型都成立。</p><h3 id="15-3-适合场景三：基础工具库">15.3 适合场景三：基础工具库</h3><p>如果你在写一个可复用组件，例如：</p><ul><li>通用缓存</li><li>结果封装类型</li><li>分页结构</li><li>Optional / Result 类结构</li></ul><p>泛型也会很自然。</p><hr><h2 id="16-什么时候不适合用泛型">16. 什么时候不适合用泛型</h2><p>这部分比“什么时候该用”更重要。</p><h3 id="16-1-业务逻辑通常不需要为了泛型而泛型">16.1 业务逻辑通常不需要为了泛型而泛型</h3><p>比如你有：</p><ul><li>用户服务</li><li>订单服务</li><li>支付服务</li></ul><p>不要为了“高级”写成：</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="keyword">type</span> Service[T any] <span class="keyword">struct</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>除非它真的有强烈的通用性，否则只是让代码更绕。</p><h3 id="16-2-只有两三个地方重复，不一定值得抽象成泛型">16.2 只有两三个地方重复，不一定值得抽象成泛型</h3><p>例如你只有：</p><ul><li>一个 <code>MaxInt</code></li><li>一个 <code>MaxFloat</code></li></ul><p>而且项目里只会用到这两处，那其实未必有必要上泛型。</p><p>Go 的哲学一直是：</p><p><strong>为真实重复抽象，不为想象中的未来抽象。</strong></p><h3 id="16-3-如果接口更自然，就不要硬上泛型">16.3 如果接口更自然，就不要硬上泛型</h3><p>比如你关心的是：</p><ul><li>文件、网络连接、内存缓冲都能“读取”</li></ul><p>这时候最自然的是 <code>io.Reader</code> 这种接口抽象，而不是泛型。</p><h3 id="16-4-如果用了泛型反而更难读，就停下来">16.4 如果用了泛型反而更难读，就停下来</h3><p>这是最实用的一条判断标准。</p><p>如果你写完后出现：</p><ul><li>类型参数三四个</li><li>约束很长</li><li>调用点也不好理解</li></ul><p>那就要反思：</p><p>这份抽象到底是在减少复杂度，还是在转移复杂度。</p><hr><h2 id="17-常见坑总结">17. 常见坑总结</h2><h3 id="17-1-以为-any-就能做任意操作">17.1 以为 <code>any</code> 就能做任意操作</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">AddOne</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(v T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">return</span> v + <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>原因：</p><ul><li><code>any</code> 没有限制类型能力</li><li>编译器无法确认 <code>+</code> 是否合法</li></ul><p>正确思路：</p><ul><li>使用数值约束</li><li>或者重新思考是否真的需要泛型</li></ul><h3 id="17-2-约束写得太宽，函数体里却用了受限操作">17.2 约束写得太宽，函数体里却用了受限操作</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Max</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(a, b T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里 <code>T any</code> 太宽了，和函数体需求不匹配。</p><p>记住一句话：</p><p><strong>约束必须和函数内部使用的操作一致。</strong></p><h3 id="17-3-为业务代码滥用泛型">17.3 为业务代码滥用泛型</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Repository[T any] <span class="keyword">struct</span> &#123;</span><br><span class="line">    items []T</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你的项目只有一种明确实体，强行写成泛型，通常只会让代码更难看。</p><p>泛型的价值在“通用复用”，不是“所有结构都必须参数化”。</p><h3 id="17-4-忘了-comparable-才能做相等比较">17.4 忘了 <code>comparable</code> 才能做相等比较</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Contains</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是错的，因为 <code>T any</code> 不能保证支持 <code>==</code>。</p><p>应该改成：</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="function"><span class="keyword">func</span> <span class="title">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span></span><br></pre></td></tr></table></figure><h3 id="17-5-看见-就慌">17.5 看见 <code>~</code> 就慌</h3><p>其实初学阶段只要记住：</p><ul><li>不加 <code>~</code>：只接受精确列出的类型</li><li>加了 <code>~</code>：允许底层类型相同的自定义类型</li></ul><p>先记住这个实用层理解就够了。</p><h3 id="17-6-过早设计特别复杂的约束体系">17.6 过早设计特别复杂的约束体系</h3><p>例如刚学泛型，就写出非常长的嵌套约束、多个类型参数、复杂接口组合。</p><p>结果是：</p><ul><li>自己两周后都看不懂</li><li>别人更不敢改</li></ul><p>建议：</p><ul><li>先学会 <code>any</code></li><li>再学 <code>comparable</code></li><li>再学简单自定义约束</li></ul><p>一步一步来。</p><hr><h2 id="18-一个完整的综合示例">18. 一个完整的综合示例</h2><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></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="keyword">type</span> Ordered <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</span> | ~<span class="type">int8</span> | ~<span class="type">int16</span> | ~<span class="type">int32</span> | ~<span class="type">int64</span> |</span><br><span class="line">        ~<span class="type">uint</span> | ~<span class="type">uint8</span> | ~<span class="type">uint16</span> | ~<span class="type">uint32</span> | ~<span class="type">uint64</span> | ~<span class="type">uintptr</span> |</span><br><span class="line">        ~<span class="type">float32</span> | ~<span class="type">float64</span> | ~<span class="type">string</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">Max</span>[<span class="title">T</span> <span class="title">Ordered</span>]<span class="params">(a, b T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</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">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Stack[T any] <span class="keyword">struct</span> &#123;</span><br><span class="line">    items []T</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">(s *Stack[T])</span></span> Push(v T) &#123;</span><br><span class="line">    s.items = <span class="built_in">append</span>(s.items, v)</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">(s *Stack[T])</span></span> Pop() (T, <span class="type">bool</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(s.items) == <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">var</span> zero T</span><br><span class="line">        <span class="keyword">return</span> zero, <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">    idx := <span class="built_in">len</span>(s.items) - <span class="number">1</span></span><br><span class="line">    value := s.items[idx]</span><br><span class="line">    s.items = s.items[:idx]</span><br><span class="line">    <span class="keyword">return</span> value, <span class="literal">true</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">    fmt.Println(Max(<span class="number">10</span>, <span class="number">20</span>))</span><br><span class="line">    fmt.Println(Max(<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;zoo&quot;</span>))</span><br><span class="line"></span><br><span class="line">    fmt.Println(Contains([]<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;, <span class="number">2</span>))</span><br><span class="line">    fmt.Println(Contains([]<span class="type">string</span>&#123;<span class="string">&quot;Go&quot;</span>, <span class="string">&quot;Java&quot;</span>&#125;, <span class="string">&quot;Rust&quot;</span>))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> s Stack[<span class="type">string</span>]</span><br><span class="line">    s.Push(<span class="string">&quot;A&quot;</span>)</span><br><span class="line">    s.Push(<span class="string">&quot;B&quot;</span>)</span><br><span class="line">    v, ok := s.Pop()</span><br><span class="line">    fmt.Println(v, ok)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子对应了三类最典型的泛型用途：</p><ul><li><code>Max</code>：通用算法 + 有序约束</li><li><code>Contains</code>：通用算法 + 可比较约束</li><li><code>Stack[T]</code>：通用数据结构</li></ul><p>如果你能把这三种写法看明白，说明泛型入门已经掌握住主干了。</p><hr><h2 id="19-本课练习">19. 本课练习</h2><h3 id="练习-1：写一个泛型-Min">练习 1：写一个泛型 Min</h3><p>仿照 <code>Max</code>，写一个 <code>Min[T Ordered](a, b T) T</code>，要求支持：</p><ul><li><code>int</code></li><li><code>float64</code></li><li><code>string</code></li></ul><hr><h3 id="练习-2：写一个泛型切片反转函数">练习 2：写一个泛型切片反转函数</h3><p>实现：</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="function"><span class="keyword">func</span> <span class="title">ReverseSlice</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(items []T)</span></span> []T</span><br></pre></td></tr></table></figure><p>要求：</p><ul><li>返回一个新的切片</li><li>不修改原切片</li><li>测试 <code>[]int</code>、<code>[]string</code>、空切片三种情况</li></ul><hr><h3 id="练习-3：写一个泛型-Set">练习 3：写一个泛型 Set</h3><p>实现：</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="keyword">type</span> Set[T comparable] <span class="keyword">map</span>[T]<span class="keyword">struct</span>&#123;&#125;</span><br></pre></td></tr></table></figure><p>并补充方法：</p><ul><li><code>Add</code></li><li><code>Remove</code></li><li><code>Has</code></li><li><code>Len</code></li></ul><p>然后分别用 <code>string</code> 和 <code>int</code> 测试。</p><hr><h3 id="练习-4：写一个泛型栈">练习 4：写一个泛型栈</h3><p>实现：</p><ul><li><code>Push</code></li><li><code>Pop</code></li><li><code>Peek</code></li><li><code>Len</code></li><li><code>IsEmpty</code></li></ul><p>要求：</p><ul><li><code>Pop</code> 和 <code>Peek</code> 在空栈时返回零值和 <code>false</code></li><li>分别用 <code>int</code>、<code>string</code> 做示例</li></ul><hr><h3 id="练习-5：接口和泛型配合练习">练习 5：接口和泛型配合练习</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Stringer <span class="keyword">interface</span> &#123;</span><br><span class="line">    String() <span class="type">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再写一个泛型函数：</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="function"><span class="keyword">func</span> <span class="title">JoinStrings</span>[<span class="title">T</span> <span class="title">Stringer</span>]<span class="params">(items []T)</span></span> <span class="type">string</span></span><br></pre></td></tr></table></figure><p>要求：</p><ul><li>把所有元素的 <code>String()</code> 结果用逗号拼起来</li><li>至少定义两个实现了 <code>String()</code> 的结构体类型进行测试</li></ul><hr><h2 id="20-自测题">20. 自测题</h2><h3 id="20-1-概念题">20.1 概念题</h3><ol><li>Go 泛型主要是为了解决什么问题？</li><li>类型参数和约束分别是什么？它们的角色有什么不同？</li><li><code>any</code> 和 <code>interface&#123;&#125;</code> 的关系是什么？</li><li>为什么 <code>Contains</code> 这类函数通常要用 <code>comparable</code> 约束？</li><li><code>~int</code> 中的 <code>~</code> 表示什么含义？</li><li>泛型和接口的核心区别是什么？各自更适合解决什么问题？</li><li>为什么说业务代码不应该为了“高级感”而滥用泛型？</li><li><code>var zero T</code> 这个写法通常用来解决什么问题？</li></ol><h3 id="20-2-代码阅读题">20.2 代码阅读题</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><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">Max</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(a, b T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</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">Contains</span>[<span class="title">T</span> <span class="title">any</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p><strong>问题 1：<code>Max</code> 的约束写错了。</strong></p><ul><li><code>T any</code> 太宽，不能保证 <code>&gt;</code> 操作合法</li><li>应该把 <code>T</code> 约束为有序类型，例如 <code>Ordered</code></li></ul><p><strong>问题 2：<code>Contains</code> 的约束写错了。</strong></p><ul><li><code>T any</code> 不能保证 <code>==</code> 合法</li><li>应该改成 <code>T comparable</code></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Ordered <span class="keyword">interface</span> &#123;</span><br><span class="line">    ~<span class="type">int</span> | ~<span class="type">int8</span> | ~<span class="type">int16</span> | ~<span class="type">int32</span> | ~<span class="type">int64</span> |</span><br><span class="line">        ~<span class="type">uint</span> | ~<span class="type">uint8</span> | ~<span class="type">uint16</span> | ~<span class="type">uint32</span> | ~<span class="type">uint64</span> | ~<span class="type">uintptr</span> |</span><br><span class="line">        ~<span class="type">float32</span> | ~<span class="type">float64</span> | ~<span class="type">string</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">Max</span>[<span class="title">T</span> <span class="title">Ordered</span>]<span class="params">(a, b T)</span></span> T &#123;</span><br><span class="line">    <span class="keyword">if</span> a &gt; b &#123;</span><br><span class="line">        <span class="keyword">return</span> a</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> b</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">Contains</span>[<span class="title">T</span> <span class="title">comparable</span>]<span class="params">(items []T, target T)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></details><hr><h2 id="21-本课总结">21. 本课总结</h2><p>这一课你已经进入了 Go 高级能力的入口。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>泛型目标</td><td>复用通用逻辑，同时保持类型安全</td></tr><tr><td>类型参数</td><td>用 <code>[T ...]</code> 表示类型变量</td></tr><tr><td>约束</td><td>限定 <code>T</code> 能参与哪些操作</td></tr><tr><td>常见约束</td><td><code>any</code>、<code>comparable</code>、自定义类型集合</td></tr><tr><td><code>~</code></td><td>允许底层类型相同的自定义类型参与</td></tr><tr><td>典型用途</td><td>通用算法、通用数据结构、基础工具库</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>泛型不是替代接口，而是补充接口</strong></li><li><strong>约束必须和函数体里要做的操作匹配</strong></li><li><strong>泛型最适合解决“同一逻辑适用于多种类型”的问题</strong></li><li><strong>如果用了泛型反而更绕、更难读，就说明这次抽象大概率不值得</strong></li></ol><hr><h2 id="22-下一课预告">22. 下一课预告</h2><p>你已经掌握了 Go 泛型的入门思路。下一步，我们来学另一个非常强大但必须谨慎使用的能力：反射。</p><p>下一课：<strong>反射入门</strong></p><p>会重点讲：</p><ul><li><code>reflect.Type</code> 和 <code>reflect.Value</code> 是什么</li><li>怎么在运行时查看变量的类型和值</li><li>反射可以做什么，代价是什么</li><li>常见反射代码该怎么读</li><li>为什么很多框架喜欢用反射，又为什么业务代码里要慎用</li></ul><p>学完下一课，你就能开始读懂很多框架和高级库里的“动态处理”代码了。</p>]]></content>
    
    
    <summary type="html">Go语言系列34</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 33 课：项目结构与工程组织</title>
    <link href="https://yjyrichard.github.io/posts/c3aa55cd.html"/>
    <id>https://yjyrichard.github.io/posts/c3aa55cd.html</id>
    <published>2026-03-22T15:28:11.751Z</published>
    <updated>2026-03-22T15:40:40.029Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 33 课：项目结构与工程组织</h1><blockquote><p>学习定位：这是整套 Go 教程的第 33 课，也是阶段五（并发与工程阶段）的第九课。<br>前置要求：已经完成第 32 课，掌握了 <code>go.mod</code>、包管理、错误处理、测试与 Benchmark 的基础能力。<br>本课目标：理解 Go 项目从“代码能跑”到“结构清晰、易维护”的组织方法，掌握常见目录结构、包拆分原则、<code>cmd</code>/<code>internal</code> 的用途，学会把配置、日志和错误包装纳入统一工程结构，并能从零搭出一个小型 Go 工程骨架。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>写到现在，你已经会写函数、结构体、接口、并发、测试和性能测试了。</p><p>但真正开始做项目时，你很快会碰到另一个层面的问题：</p><ul><li><code>main.go</code> 越写越长，几百行甚至上千行</li><li>文件越来越多，却不知道该怎么拆包</li><li>配置到处读，环境变量满天飞</li><li>日志有的用 <code>fmt.Println</code>，有的用 <code>log.Println</code>，风格不统一</li><li>错误只会 <code>return err</code>，一旦报错根本不知道是哪一层出的事</li><li>包之间互相 import，最后出现循环依赖</li></ul><p>这时候你会发现：<strong>会写 Go 代码，不等于会组织 Go 项目。</strong></p><p>项目结构与工程组织，解决的是这几个问题：</p><ul><li>代码应该按什么维度拆分</li><li>哪些东西放在根目录，哪些放到子目录</li><li>业务逻辑、配置、日志、存储逻辑分别放哪</li><li>怎么让包依赖关系保持清晰</li><li>怎么让项目以后继续长大时不失控</li></ul><p>这一课的重点不是“背目录模板”，而是理解<strong>为什么要这样组织</strong>。</p><hr><h2 id="2-先建立一个正确认识：Go-工程结构不是越复杂越高级">2. 先建立一个正确认识：Go 工程结构不是越复杂越高级</h2><p>很多人刚接触工程化，就喜欢先抄一个很大的目录模板：</p><figure class="highlight text"><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">cmd/</span><br><span class="line">internal/</span><br><span class="line">pkg/</span><br><span class="line">api/</span><br><span class="line">configs/</span><br><span class="line">scripts/</span><br><span class="line">build/</span><br><span class="line">deploy/</span><br><span class="line">docs/</span><br><span class="line">tools/</span><br><span class="line">third_party/</span><br></pre></td></tr></table></figure><p>然后项目里只有 3 个 <code>.go</code> 文件。</p><p>这就是典型的<strong>过度设计</strong>。</p><p>Go 社区对项目结构有一个很重要的共识：</p><blockquote><p>结构要服务于规模，而不是为了看起来专业。</p></blockquote><h3 id="2-1-小项目可以很简单">2.1 小项目可以很简单</h3><p>如果你只是写一个练手的小工具，这样完全没问题：</p><figure class="highlight text"><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">hello-go/</span><br><span class="line">├── go.mod</span><br><span class="line">├── main.go</span><br><span class="line">└── config.json</span><br></pre></td></tr></table></figure><p>只要代码不多、职责不乱，这就是合理结构。</p><h3 id="2-2-项目一长大，结构问题就会暴露">2.2 项目一长大，结构问题就会暴露</h3><p>假设你写了一个任务管理 CLI，最开始只有 1 个文件：</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="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">// 解析参数</span></span><br><span class="line">    <span class="comment">// 读取配置</span></span><br><span class="line">    <span class="comment">// 初始化日志</span></span><br><span class="line">    <span class="comment">// 读文件</span></span><br><span class="line">    <span class="comment">// 解析 JSON</span></span><br><span class="line">    <span class="comment">// 增删改查任务</span></span><br><span class="line">    <span class="comment">// 打印结果</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一开始还行，三天后就会变成这样：</p><ul><li><code>main</code> 负责所有事情</li><li>文件读写和业务逻辑混在一起</li><li>输出、日志、错误处理混在一起</li><li>想加测试时发现逻辑都在 <code>main</code> 里，很难测</li></ul><p>所以，项目结构的真正目标不是“好看”，而是：</p><ol><li>让职责分开</li><li>让依赖方向清晰</li><li>让代码方便测试</li><li>让后续迭代不容易失控</li></ol><h3 id="2-3-一个实用判断标准">2.3 一个实用判断标准</h3><p>什么时候应该开始拆结构？</p><p>当你出现下面任意一种情况时，就该动手了：</p><ul><li>一个文件超过 200～300 行，还在继续增长</li><li>一个包里既有业务逻辑，又有存储逻辑，又有命令行解析</li><li>多个功能开始共享同一段逻辑</li><li>需要给某一层单独写测试</li><li>你自己隔一周回来已经不容易快速定位代码</li></ul><p>记住一句话：</p><p><strong>先让代码跑起来，再在增长点上做结构化拆分。</strong></p><hr><h2 id="3-Go-项目结构的三种常见层级">3. Go 项目结构的三种常见层级</h2><p>这部分不要死记，重点是理解“项目规模变化，结构怎么跟着演进”。</p><h3 id="3-1-第一层：单文件项目">3.1 第一层：单文件项目</h3><p>适合场景：</p><ul><li>学习示例</li><li>一次性脚本</li><li>非常小的 CLI 工具</li></ul><p>结构：</p><figure class="highlight text"><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">project/</span><br><span class="line">├── go.mod</span><br><span class="line">└── main.go</span><br></pre></td></tr></table></figure><p>优点：</p><ul><li>最简单，零心智负担</li><li>新手最容易启动</li></ul><p>缺点：</p><ul><li>很快堆成“大杂烩”</li><li>几乎没有可维护性</li></ul><h3 id="3-2-第二层：多文件、同包项目">3.2 第二层：多文件、同包项目</h3><p>适合场景：</p><ul><li>小型练手项目</li><li>功能开始变多，但还没复杂到拆多个包</li></ul><p>结构：</p><figure class="highlight text"><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">project/</span><br><span class="line">├── go.mod</span><br><span class="line">├── main.go</span><br><span class="line">├── config.go</span><br><span class="line">├── task.go</span><br><span class="line">├── store.go</span><br><span class="line">└── output.go</span><br></pre></td></tr></table></figure><p>这些文件都属于同一个 <code>package main</code>。</p><p>优点：</p><ul><li>比单文件清晰</li><li>不需要处理跨包依赖</li></ul><p>缺点：</p><ul><li>仍然没有真正的边界</li><li>逻辑很容易继续耦合</li><li>测试和复用能力有限</li></ul><h3 id="3-3-第三层：多包工程项目">3.3 第三层：多包工程项目</h3><p>适合场景：</p><ul><li>需要长期维护</li><li>业务逻辑、存储逻辑、配置、日志开始分层</li><li>希望结构清晰、可测、可扩展</li></ul><p>结构示意：</p><figure class="highlight text"><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">project/</span><br><span class="line">├── go.mod</span><br><span class="line">├── cmd/</span><br><span class="line">│   └── app/</span><br><span class="line">│       └── main.go</span><br><span class="line">└── internal/</span><br><span class="line">    ├── config/</span><br><span class="line">    ├── logger/</span><br><span class="line">    ├── service/</span><br><span class="line">    ├── store/</span><br><span class="line">    └── model/</span><br></pre></td></tr></table></figure><p>这就是我们这课要重点掌握的层级。</p><hr><h2 id="4-常见目录到底是干什么的">4. 常见目录到底是干什么的</h2><p>下面这些目录是 Go 项目里最常见的。注意：<strong>常见，不等于必须全用。</strong></p><h3 id="4-1-根目录">4.1 根目录</h3><p>根目录通常放项目级别的信息：</p><figure class="highlight text"><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">project/</span><br><span class="line">├── go.mod</span><br><span class="line">├── go.sum</span><br><span class="line">├── README.md</span><br><span class="line">└── .gitignore</span><br></pre></td></tr></table></figure><p>常见文件作用：</p><table><thead><tr><th>文件</th><th>作用</th></tr></thead><tbody><tr><td><code>go.mod</code></td><td>模块名、Go 版本、依赖声明</td></tr><tr><td><code>go.sum</code></td><td>依赖校验信息</td></tr><tr><td><code>README.md</code></td><td>项目说明、运行方式</td></tr><tr><td><code>.gitignore</code></td><td>Git 忽略规则</td></tr></tbody></table><p>根目录通常<strong>不应该堆很多业务代码</strong>。如果项目进入工程化阶段，业务代码最好放进更明确的目录中。</p><h3 id="4-2-cmd">4.2 <code>cmd/</code></h3><p><code>cmd</code> 用来放<strong>可执行程序入口</strong>。</p><p>例如：</p><figure class="highlight text"><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">cmd/</span><br><span class="line">├── todo/</span><br><span class="line">│   └── main.go</span><br><span class="line">└── migrate/</span><br><span class="line">    └── main.go</span><br></pre></td></tr></table></figure><p>说明：</p><ul><li><code>cmd/todo/main.go</code> 是任务管理 CLI 的入口</li><li><code>cmd/migrate/main.go</code> 是数据库迁移工具入口</li></ul><p>如果一个仓库里有多个可执行程序，<code>cmd</code> 特别好用。</p><p>一个很重要的实践是：</p><blockquote><p><code>main.go</code> 负责组装依赖和启动程序，不负责承载业务细节。</p></blockquote><p>也就是说，<code>main.go</code> 应该更像“接线员”，而不是“业务实现中心”。</p><h3 id="4-3-internal">4.3 <code>internal/</code></h3><p><code>internal</code> 是 Go 很有特色的一个目录。</p><p>放进 <code>internal</code> 的包，只允许<strong>当前模块内部</strong>导入，外部模块不能导入。</p><p>例如项目结构：</p><figure class="highlight text"><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">example.com/todo-cli/</span><br><span class="line">├── cmd/todo/main.go</span><br><span class="line">└── internal/task/service.go</span><br></pre></td></tr></table></figure><p>当前模块内部可以导入：</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="keyword">import</span> <span class="string">&quot;example.com/todo-cli/internal/task&quot;</span></span><br></pre></td></tr></table></figure><p>但其他项目如果想导入这个路径，会直接编译失败。</p><p>这意味着：</p><ul><li><code>internal</code> 适合放你的内部实现细节</li><li>可以避免别人把你的内部包当公共 API 使用</li><li>能强制建立“这是内部实现，不对外承诺稳定性”的边界</li></ul><h3 id="4-4-pkg">4.4 <code>pkg/</code></h3><p><code>pkg</code> 不是 Go 语言强制规定的目录，只是一种约定。</p><p>常见理解是：</p><ul><li><code>internal/</code> 放内部实现</li><li><code>pkg/</code> 放希望被外部复用的公共包</li></ul><p>但要注意，<code>pkg/</code> 并不是必须的，很多项目根本不用它。</p><h4 id="什么时候适合用-pkg">什么时候适合用 <code>pkg/</code></h4><ul><li>你明确要提供一个可复用库</li><li>你希望外部项目可以 import 你的某些包</li></ul><h4 id="什么时候不建议一上来就用-pkg">什么时候不建议一上来就用 <code>pkg/</code></h4><ul><li>你还不确定哪些代码会被外部复用</li><li>项目本质是应用，而不是库</li><li>你只是为了“看起来像大项目”而加 <code>pkg/</code></li></ul><p>对初学者来说，更推荐的原则是：</p><p><strong>应用型项目优先考虑 <code>cmd + internal</code>，不要机械套 <code>pkg</code>。</strong></p><h3 id="4-5-configs-、scripts-、testdata">4.5 <code>configs/</code>、<code>scripts/</code>、<code>testdata/</code></h3><p>这些目录也是常见的，但都是“按需使用”。</p><table><thead><tr><th>目录</th><th>常见用途</th></tr></thead><tbody><tr><td><code>configs/</code></td><td>示例配置文件、模板配置</td></tr><tr><td><code>scripts/</code></td><td>构建、发布、初始化脚本</td></tr><tr><td><code>testdata/</code></td><td>测试输入文件，Go 测试约定会忽略它</td></tr></tbody></table><p>例如：</p><figure class="highlight text"><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">project/</span><br><span class="line">├── internal/</span><br><span class="line">├── cmd/</span><br><span class="line">├── testdata/</span><br><span class="line">│   └── sample.json</span><br><span class="line">└── scripts/</span><br><span class="line">    └── release.sh</span><br></pre></td></tr></table></figure><p>如果你当前项目根本没有这些需求，就不要先建空目录。</p><hr><h2 id="5-包怎么拆？先看两种常见思路">5. 包怎么拆？先看两种常见思路</h2><p>这是工程组织里最关键的地方。</p><p>同一个项目，包拆法可能完全不同。没有绝对唯一答案，但有优劣。</p><h3 id="5-1-方案-A：按技术职责拆包">5.1 方案 A：按技术职责拆包</h3><p>例如：</p><figure class="highlight text"><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">internal/</span><br><span class="line">├── config/</span><br><span class="line">├── logger/</span><br><span class="line">├── model/</span><br><span class="line">├── service/</span><br><span class="line">└── store/</span><br></pre></td></tr></table></figure><p>特点：</p><ul><li><code>config</code> 管配置</li><li><code>logger</code> 管日志</li><li><code>model</code> 放数据结构</li><li><code>service</code> 放业务逻辑</li><li><code>store</code> 放文件、数据库等存储实现</li></ul><p>优点：</p><ul><li>对初学者非常直观</li><li>依赖关系容易理解</li><li>很适合单一业务的小项目</li></ul><p>缺点：</p><ul><li>项目一大，和同一业务相关的代码会分散在很多目录里</li><li>找一个功能需要跨多个目录跳来跳去</li></ul><h3 id="5-2-方案-B：按业务领域拆包">5.2 方案 B：按业务领域拆包</h3><p>例如：</p><figure class="highlight text"><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">internal/</span><br><span class="line">├── config/</span><br><span class="line">├── logger/</span><br><span class="line">├── task/</span><br><span class="line">├── user/</span><br><span class="line">└── report/</span><br></pre></td></tr></table></figure><p>其中 <code>task/</code> 里可以同时放：</p><ul><li>任务数据结构</li><li>任务业务逻辑</li><li>任务相关接口定义</li></ul><p>优点：</p><ul><li>高内聚，围绕业务组织</li><li>功能扩展时更自然</li><li>中大型项目更常见</li></ul><p>缺点：</p><ul><li>对新手来说不如“按职责拆包”直观</li><li>共享基础能力需要额外抽象</li></ul><h3 id="5-3-这一课推荐哪种">5.3 这一课推荐哪种</h3><p>对于你当前这个阶段，我建议这样理解：</p><ol><li><strong>单一业务、体量不大</strong>：按技术职责拆包更容易上手</li><li><strong>业务开始明显分块</strong>：按业务领域拆包更利于长期维护</li></ol><p>这一课的完整示例会采用一种折中方式：</p><ul><li><code>config</code>、<code>logger</code>、<code>store</code> 这类明显的基础设施单独拆出</li><li>核心业务收拢到 <code>task</code> 包里</li></ul><p>这比“纯技术分层”更贴近真实项目，也比“完全按领域”更容易理解。</p><hr><h2 id="6-包拆分的五条实用原则">6. 包拆分的五条实用原则</h2><p>不管你按哪种思路拆，下面这五条都非常重要。</p><h3 id="6-1-一包一职责">6.1 一包一职责</h3><p>一个包最好有明确边界。</p><p>例如：</p><ul><li><code>config</code>：只负责配置加载和校验</li><li><code>logger</code>：只负责日志初始化</li><li><code>task</code>：只负责任务领域逻辑</li><li><code>store</code>：只负责持久化实现</li></ul><p>反例是这种“万能包”：</p><figure class="highlight text"><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">internal/</span><br><span class="line">└── util/</span><br></pre></td></tr></table></figure><p>几年经验总结下来，<code>util</code>、<code>common</code>、<code>helper</code> 这类名字往往意味着：</p><ul><li>边界模糊</li><li>什么都能往里塞</li><li>后面一定越来越乱</li></ul><h3 id="6-2-依赖方向要单向">6.2 依赖方向要单向</h3><p>好的依赖关系通常是：</p><figure class="highlight text"><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">main -&gt; config/logger/store/task</span><br><span class="line">store -&gt; task</span><br><span class="line">task -&gt; 标准库</span><br></pre></td></tr></table></figure><p>坏的依赖关系通常是互相 import：</p><figure class="highlight text"><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">task -&gt; store</span><br><span class="line">store -&gt; task</span><br></pre></td></tr></table></figure><p>一旦这样写，就很容易出现循环依赖。</p><p>Go 明确禁止循环 import，这是件好事，因为它逼着你把结构整理清楚。</p><h3 id="6-3-接口尽量定义在使用方">6.3 接口尽量定义在使用方</h3><p>比如任务服务需要一个“能加载和保存任务”的存储能力，那么接口更适合定义在 <code>task</code> 包中：</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">type</span> Repository <span class="keyword">interface</span> &#123;</span><br><span class="line">    Load() ([]Task, <span class="type">error</span>)</span><br><span class="line">    Save([]Task) <span class="type">error</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么？</p><p>因为是 <code>task</code> 服务在“消费”这项能力，它最清楚自己需要什么方法。</p><p>这样做的好处：</p><ul><li>接口更小、更聚焦</li><li>存储层更容易替换</li><li>更方便测试时做 mock</li></ul><h3 id="6-4-包名要短、准、自然">6.4 包名要短、准、自然</h3><p>推荐：</p><ul><li><code>task</code></li><li><code>store</code></li><li><code>config</code></li><li><code>logger</code></li></ul><p>不推荐：</p><ul><li><code>taskservice</code></li><li><code>taskmanager</code></li><li><code>commonutils</code></li><li><code>task_model_package</code></li></ul><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></pre></td><td class="code"><pre><span class="line">task.NewService()</span><br><span class="line">config.Load()</span><br><span class="line">logger.New()</span><br></pre></td></tr></table></figure><p>如果包名太长，代码会非常啰嗦。</p><h3 id="6-5-导出越少越好">6.5 导出越少越好</h3><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">type</span> Service <span class="keyword">struct</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewService</span><span class="params">(...)</span></span> *Service &#123; ... &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Service)</span></span> Add(...) <span class="type">error</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>像 <code>nextTaskID</code> 这种纯内部辅助函数，就保持小写：</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="function"><span class="keyword">func</span> <span class="title">nextTaskID</span><span class="params">(tasks []Task)</span></span> <span class="type">int</span> &#123; ... &#125;</span><br></pre></td></tr></table></figure><p>导出太多，会让包的表面 API 变大，维护成本上升。</p><hr><h2 id="7-一个完整的小型工程示例">7. 一个完整的小型工程示例</h2><p>下面我们用“任务管理 CLI”作为例子，搭一个结构清晰的小项目。</p><h3 id="7-1-最终目录结构">7.1 最终目录结构</h3><figure class="highlight text"><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></pre></td><td class="code"><pre><span class="line">todo-cli/</span><br><span class="line">├── go.mod</span><br><span class="line">├── README.md</span><br><span class="line">├── cmd/</span><br><span class="line">│   └── todo/</span><br><span class="line">│       └── main.go</span><br><span class="line">└── internal/</span><br><span class="line">    ├── config/</span><br><span class="line">    │   └── config.go</span><br><span class="line">    ├── logger/</span><br><span class="line">    │   └── logger.go</span><br><span class="line">    ├── store/</span><br><span class="line">    │   └── file_store.go</span><br><span class="line">    └── task/</span><br><span class="line">        ├── task.go</span><br><span class="line">        └── service.go</span><br></pre></td></tr></table></figure><p>这个结构里，每一层职责都比较清晰：</p><ul><li><code>cmd/todo/main.go</code>：程序入口，解析参数，组装依赖</li><li><code>internal/config</code>：读取配置</li><li><code>internal/logger</code>：初始化日志</li><li><code>internal/store</code>：任务数据的文件存储实现</li><li><code>internal/task</code>：任务领域模型与业务逻辑</li></ul><h3 id="7-2-初始化模块">7.2 初始化模块</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go mod init example.com/todo-cli</span><br></pre></td></tr></table></figure><p>这里的模块名只是示意，你实际项目可以换成自己的仓库地址。</p><hr><h2 id="8-先定义业务包：internal-task">8. 先定义业务包：<code>internal/task</code></h2><p>业务包通常应该先建，因为它代表项目最核心的能力。</p><h3 id="8-1-任务模型">8.1 任务模型</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// internal/task/task.go</span></span><br><span class="line"><span class="keyword">package</span> task</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Task 表示一条任务记录</span></span><br><span class="line"><span class="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID        <span class="type">int</span>       <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Title     <span class="type">string</span>    <span class="string">`json:&quot;title&quot;`</span></span><br><span class="line">    Done      <span class="type">bool</span>      <span class="string">`json:&quot;done&quot;`</span></span><br><span class="line">    CreatedAt time.Time <span class="string">`json:&quot;created_at&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-2-服务层">8.2 服务层</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// internal/task/service.go</span></span><br><span class="line"><span class="keyword">package</span> task</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 class="string">&quot;log/slog&quot;</span></span><br><span class="line">    <span class="string">&quot;strings&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="keyword">var</span> ErrTaskNotFound = errors.New(<span class="string">&quot;任务不存在&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Repository 定义任务服务依赖的存储能力</span></span><br><span class="line"><span class="keyword">type</span> Repository <span class="keyword">interface</span> &#123;</span><br><span class="line">    Load() ([]Task, <span class="type">error</span>)</span><br><span class="line">    Save([]Task) <span class="type">error</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Service 封装任务相关业务逻辑</span></span><br><span class="line"><span class="keyword">type</span> Service <span class="keyword">struct</span> &#123;</span><br><span class="line">    repo   Repository</span><br><span class="line">    logger *slog.Logger</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewService 创建任务服务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewService</span><span class="params">(repo Repository, logger *slog.Logger)</span></span> *Service &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;Service&#123;</span><br><span class="line">        repo:   repo,</span><br><span class="line">        logger: logger,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add 新增任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Service)</span></span> Add(title <span class="type">string</span>) (Task, <span class="type">error</span>) &#123;</span><br><span class="line">    title = strings.TrimSpace(title)</span><br><span class="line">    <span class="keyword">if</span> title == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Task&#123;&#125;, errors.New(<span class="string">&quot;任务标题不能为空&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tasks, err := s.repo.Load()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Task&#123;&#125;, fmt.Errorf(<span class="string">&quot;加载任务列表失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    item := Task&#123;</span><br><span class="line">        ID:        nextTaskID(tasks),</span><br><span class="line">        Title:     title,</span><br><span class="line">        Done:      <span class="literal">false</span>,</span><br><span class="line">        CreatedAt: time.Now(),</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tasks = <span class="built_in">append</span>(tasks, item)</span><br><span class="line">    <span class="keyword">if</span> err := s.repo.Save(tasks); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Task&#123;&#125;, fmt.Errorf(<span class="string">&quot;保存任务失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    s.logger.Info(<span class="string">&quot;新增任务成功&quot;</span>, <span class="string">&quot;id&quot;</span>, item.ID, <span class="string">&quot;title&quot;</span>, item.Title)</span><br><span class="line">    <span class="keyword">return</span> item, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// List 返回所有任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Service)</span></span> List() ([]Task, <span class="type">error</span>) &#123;</span><br><span class="line">    tasks, err := s.repo.Load()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;读取任务列表失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> tasks, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Done 将指定任务标记为已完成</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Service)</span></span> Done(id <span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> id &lt;= <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> errors.New(<span class="string">&quot;任务 ID 必须大于 0&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tasks, err := s.repo.Load()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;读取任务列表失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    found := <span class="literal">false</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> tasks[i].ID == id &#123;</span><br><span class="line">            tasks[i].Done = <span class="literal">true</span></span><br><span class="line">            found = <span class="literal">true</span></span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !found &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;标记任务完成失败: %w&quot;</span>, ErrTaskNotFound)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> err := s.repo.Save(tasks); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;写回任务数据失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    s.logger.Info(<span class="string">&quot;任务已完成&quot;</span>, <span class="string">&quot;id&quot;</span>, id)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</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">nextTaskID</span><span class="params">(tasks []Task)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    maxID := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> item.ID &gt; maxID &#123;</span><br><span class="line">            maxID = item.ID</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> maxID + <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个包体现了几个关键点：</p><ul><li><strong>业务逻辑集中在 <code>task</code> 包</strong></li><li><strong>存储能力通过接口抽象</strong></li><li><strong>错误在当前语义层补充上下文</strong></li><li><strong>日志记录业务事件，但不吞掉错误</strong></li></ul><hr><h2 id="9-再实现存储层：internal-store">9. 再实现存储层：<code>internal/store</code></h2><p>现在我们给 <code>task.Service</code> 提供一个文件存储实现。</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><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// internal/store/file_store.go</span></span><br><span class="line"><span class="keyword">package</span> store</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;encoding/json&quot;</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 class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;example.com/todo-cli/internal/task&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// FileStore 使用 JSON 文件保存任务数据</span></span><br><span class="line"><span class="keyword">type</span> FileStore <span class="keyword">struct</span> &#123;</span><br><span class="line">    path <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewFileStore 创建文件存储</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewFileStore</span><span class="params">(path <span class="type">string</span>)</span></span> *FileStore &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;FileStore&#123;path: path&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Load 读取所有任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *FileStore)</span></span> Load() ([]task.Task, <span class="type">error</span>) &#123;</span><br><span class="line">    data, err := os.ReadFile(s.path)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> errors.Is(err, os.ErrNotExist) &#123;</span><br><span class="line">            <span class="keyword">return</span> []task.Task&#123;&#125;, <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;读取文件 %s 失败: %w&quot;</span>, s.path, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(data) == <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> []task.Task&#123;&#125;, <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> tasks []task.Task</span><br><span class="line">    <span class="keyword">if</span> err := json.Unmarshal(data, &amp;tasks); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;解析文件 %s 失败: %w&quot;</span>, s.path, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> tasks, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Save 保存所有任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *FileStore)</span></span> Save(tasks []task.Task) <span class="type">error</span> &#123;</span><br><span class="line">    dir := filepath.Dir(s.path)</span><br><span class="line">    <span class="keyword">if</span> dir != <span class="string">&quot;.&quot;</span> &amp;&amp; dir != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> err := os.MkdirAll(dir, <span class="number">0755</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;创建目录 %s 失败: %w&quot;</span>, dir, err)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    data, err := json.MarshalIndent(tasks, <span class="string">&quot;&quot;</span>, <span class="string">&quot;  &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;编码任务数据失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> err := os.WriteFile(s.path, data, <span class="number">0644</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;写入文件 %s 失败: %w&quot;</span>, s.path, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里你可以看到一个很重要的依赖方向：</p><figure class="highlight text"><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">store -&gt; task</span><br><span class="line">task 不依赖 store</span><br></pre></td></tr></table></figure><p>也就是说：</p><ul><li><code>task</code> 只知道自己需要一个 <code>Repository</code></li><li><code>store</code> 来适配这个接口</li></ul><p>这是非常经典、非常实用的工程组织方式。</p><hr><h2 id="10-配置不要到处读：统一放进-internal-config">10. 配置不要到处读：统一放进 <code>internal/config</code></h2><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></pre></td><td class="code"><pre><span class="line">dbHost := os.Getenv(<span class="string">&quot;DB_HOST&quot;</span>)</span><br><span class="line">dataFile := os.Getenv(<span class="string">&quot;TODO_DATA_FILE&quot;</span>)</span><br><span class="line">logLevel := os.Getenv(<span class="string">&quot;TODO_LOG_LEVEL&quot;</span>)</span><br></pre></td></tr></table></figure><p>然后这些逻辑散落在：</p><ul><li><code>main.go</code></li><li><code>service.go</code></li><li><code>store.go</code></li><li><code>handler.go</code></li></ul><p>这会导致两个问题：</p><ol><li>配置来源不集中，改起来很痛苦</li><li>很难知道某个环境变量到底在哪里生效</li></ol><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// internal/config/config.go</span></span><br><span class="line"><span class="keyword">package</span> config</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;os&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Config 表示程序运行配置</span></span><br><span class="line"><span class="keyword">type</span> Config <span class="keyword">struct</span> &#123;</span><br><span class="line">    DataFile <span class="type">string</span></span><br><span class="line">    LogLevel <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Load 从环境变量加载配置</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Load</span><span class="params">()</span></span> (Config, <span class="type">error</span>) &#123;</span><br><span class="line">    cfg := Config&#123;</span><br><span class="line">        DataFile: getEnv(<span class="string">&quot;TODO_DATA_FILE&quot;</span>, <span class="string">&quot;data/tasks.json&quot;</span>),</span><br><span class="line">        LogLevel: getEnv(<span class="string">&quot;TODO_LOG_LEVEL&quot;</span>, <span class="string">&quot;INFO&quot;</span>),</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> cfg.DataFile == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Config&#123;&#125;, errors.New(<span class="string">&quot;TODO_DATA_FILE 不能为空&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> cfg, <span class="literal">nil</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">getEnv</span><span class="params">(key, fallback <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    value := os.Getenv(key)</span><br><span class="line">    <span class="keyword">if</span> value == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fallback</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> value</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-1-配置设计的三条原则">10.1 配置设计的三条原则</h3><h4 id="原则-1：配置集中定义">原则 1：配置集中定义</h4><p>所有运行配置尽量收敛到一个 <code>Config</code> 结构体中。</p><p>这样你只要看这个结构体，就知道程序依赖哪些配置。</p><h4 id="原则-2：配置加载和业务逻辑分开">原则 2：配置加载和业务逻辑分开</h4><p><code>task.Service</code> 不应该自己去读环境变量。</p><p>因为：</p><ul><li>它只关心“我要一个数据文件路径”</li><li>不关心这个路径是从环境变量、命令行还是配置文件来的</li></ul><h4 id="原则-3：配置要有默认值和校验">原则 3：配置要有默认值和校验</h4><p>比如：</p><ul><li><code>LogLevel</code> 默认 <code>INFO</code></li><li><code>DataFile</code> 默认 <code>data/tasks.json</code></li><li>特别关键的字段可以显式校验</li></ul><p>这会让项目在开发环境里更容易启动。</p><hr><h2 id="11-日志不要乱打：统一放进-internal-logger">11. 日志不要乱打：统一放进 <code>internal/logger</code></h2><p>在小练习里你可以到处 <code>fmt.Println</code>，但工程里最好尽快建立统一日志方式。</p><h3 id="11-1-为什么日志要统一">11.1 为什么日志要统一</h3><p>如果项目里同时存在：</p><ul><li><code>fmt.Println</code></li><li><code>log.Println</code></li><li><code>panic</code></li><li><code>logger.Info</code></li></ul><p>那后面排查问题会非常痛苦。</p><p>统一日志至少能带来这些好处：</p><ul><li>输出格式一致</li><li>可以控制日志级别</li><li>更容易定位问题</li><li>以后切换日志实现代价更小</li></ul><h3 id="11-2-用标准库-log-slog">11.2 用标准库 <code>log/slog</code></h3><p>Go 1.21 开始，标准库提供了 <code>log/slog</code>，很适合工程项目的结构化日志需求。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// internal/logger/logger.go</span></span><br><span class="line"><span class="keyword">package</span> logger</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/slog&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;strings&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// New 根据日志级别创建 logger</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(level <span class="type">string</span>)</span></span> *slog.Logger &#123;</span><br><span class="line">    <span class="keyword">var</span> logLevel slog.Level</span><br><span class="line"></span><br><span class="line">    <span class="keyword">switch</span> strings.ToUpper(level) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;DEBUG&quot;</span>:</span><br><span class="line">        logLevel = slog.LevelDebug</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;WARN&quot;</span>:</span><br><span class="line">        logLevel = slog.LevelWarn</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;ERROR&quot;</span>:</span><br><span class="line">        logLevel = slog.LevelError</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        logLevel = slog.LevelInfo</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    handler := slog.NewTextHandler(os.Stdout, &amp;slog.HandlerOptions&#123;</span><br><span class="line">        Level: logLevel,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> slog.New(handler)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-3-日志应该记什么">11.3 日志应该记什么</h3><p>好的日志通常记录：</p><ul><li>关键业务事件</li><li>错误发生点</li><li>重要上下文信息</li></ul><p>例如：</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">s.logger.Info(<span class="string">&quot;新增任务成功&quot;</span>, <span class="string">&quot;id&quot;</span>, item.ID, <span class="string">&quot;title&quot;</span>, item.Title)</span><br></pre></td></tr></table></figure><p>这比单纯打印：</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">fmt.Println(<span class="string">&quot;add ok&quot;</span>)</span><br></pre></td></tr></table></figure><p>信息量大很多。</p><h3 id="11-4-日志的一个常见误区">11.4 日志的一个常见误区</h3><p><strong>不要每一层都打一遍同一个错误日志。</strong></p><p>错误链路如果是：</p><figure class="highlight text"><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">os.ReadFile 失败</span><br><span class="line">-&gt; store.Load 返回错误</span><br><span class="line">-&gt; task.List 返回错误</span><br><span class="line">-&gt; main 打印错误</span><br></pre></td></tr></table></figure><p>通常只需要：</p><ul><li>中间层补充错误上下文</li><li>最外层决定是否打印日志或退出程序</li></ul><p>否则你会看到同一个错误被打印 3 次。</p><hr><h2 id="12-错误包装是工程组织的重要部分">12. 错误包装是工程组织的重要部分</h2><p>这节课的“工程感”很大一部分来自错误处理方式。</p><h3 id="12-1-为什么不能只写-return-err">12.1 为什么不能只写 <code>return err</code></h3><p>假设底层报错：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">open data/tasks.json: no such file or directory</span><br></pre></td></tr></table></figure><p>如果每一层都只是 <code>return err</code>，到了最外层你只能看到这个原始错误。</p><p>你不知道：</p><ul><li>是加载配置失败</li><li>还是读取任务失败</li><li>还是保存任务失败</li></ul><h3 id="12-2-正确做法：在每一层补充语义">12.2 正确做法：在每一层补充语义</h3><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">tasks, err := s.repo.Load()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;读取任务列表失败: %w&quot;</span>, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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">if</span> err := run(service, action, title, id); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    fmt.Fprintln(os.Stderr, <span class="string">&quot;操作失败:&quot;</span>, err)</span><br><span class="line">    os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后得到的错误链会更有语义，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">操作失败: 读取任务列表失败: 读取文件 data/tasks.json 失败: 权限不足</span><br></pre></td></tr></table></figure><p>这就非常容易定位了。</p><h3 id="12-3-w-和-errors-Is">12.3 <code>%w</code> 和 <code>errors.Is</code></h3><p>错误包装时要用 <code>%w</code>：</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="keyword">return</span> fmt.Errorf(<span class="string">&quot;标记任务完成失败: %w&quot;</span>, ErrTaskNotFound)</span><br></pre></td></tr></table></figure><p>这样最外层才能用 <code>errors.Is</code> 判断：</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">if</span> errors.Is(err, task.ErrTaskNotFound) &#123;</span><br><span class="line">    fmt.Fprintln(os.Stderr, <span class="string">&quot;任务不存在&quot;</span>)</span><br><span class="line">    os.Exit(<span class="number">2</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是工程里常说的：</p><ul><li><strong>中间层负责补充上下文</strong></li><li><strong>边界层负责决定如何处理</strong></li></ul><h3 id="12-4-什么时候不该包装">12.4 什么时候不该包装</h3><p>如果当前层没有增加任何新语义，单纯套一层：</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="keyword">return</span> fmt.Errorf(<span class="string">&quot;error: %w&quot;</span>, err)</span><br></pre></td></tr></table></figure><p>其实价值不大。</p><p>包装的关键不是“多包一层”，而是“补充定位信息”。</p><hr><h2 id="13-程序入口应该长什么样">13. 程序入口应该长什么样</h2><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// cmd/todo/main.go</span></span><br><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;flag&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;example.com/todo-cli/internal/config&quot;</span></span><br><span class="line">    <span class="string">&quot;example.com/todo-cli/internal/logger&quot;</span></span><br><span class="line">    <span class="string">&quot;example.com/todo-cli/internal/store&quot;</span></span><br><span class="line">    <span class="string">&quot;example.com/todo-cli/internal/task&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="keyword">var</span> action <span class="type">string</span></span><br><span class="line">    <span class="keyword">var</span> title <span class="type">string</span></span><br><span class="line">    <span class="keyword">var</span> id <span class="type">int</span></span><br><span class="line"></span><br><span class="line">    flag.StringVar(&amp;action, <span class="string">&quot;action&quot;</span>, <span class="string">&quot;list&quot;</span>, <span class="string">&quot;操作：list/add/done&quot;</span>)</span><br><span class="line">    flag.StringVar(&amp;title, <span class="string">&quot;title&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;任务标题&quot;</span>)</span><br><span class="line">    flag.IntVar(&amp;id, <span class="string">&quot;id&quot;</span>, <span class="number">0</span>, <span class="string">&quot;任务 ID&quot;</span>)</span><br><span class="line">    flag.Parse()</span><br><span class="line"></span><br><span class="line">    cfg, err := config.Load()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        fmt.Fprintln(os.Stderr, <span class="string">&quot;加载配置失败:&quot;</span>, err)</span><br><span class="line">        os.Exit(<span class="number">1</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    log := logger.New(cfg.LogLevel)</span><br><span class="line">    repo := store.NewFileStore(cfg.DataFile)</span><br><span class="line">    service := task.NewService(repo, log)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> err := run(service, action, title, id); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> errors.Is(err, task.ErrTaskNotFound) &#123;</span><br><span class="line">            fmt.Fprintln(os.Stderr, <span class="string">&quot;操作失败:&quot;</span>, err)</span><br><span class="line">            os.Exit(<span class="number">2</span>)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        fmt.Fprintln(os.Stderr, <span class="string">&quot;操作失败:&quot;</span>, err)</span><br><span class="line">        os.Exit(<span class="number">1</span>)</span><br><span class="line">    &#125;</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">run</span><span class="params">(service *task.Service, action, title <span class="type">string</span>, id <span class="type">int</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> action &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;list&quot;</span>:</span><br><span class="line">        tasks, err := service.List()</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> err</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(tasks) == <span class="number">0</span> &#123;</span><br><span class="line">            fmt.Println(<span class="string">&quot;当前没有任务&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> _, item := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">            status := <span class="string">&quot;未完成&quot;</span></span><br><span class="line">            <span class="keyword">if</span> item.Done &#123;</span><br><span class="line">                status = <span class="string">&quot;已完成&quot;</span></span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;[%d] %s (%s)\n&quot;</span>, item.ID, item.Title, status)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;add&quot;</span>:</span><br><span class="line">        item, err := service.Add(title)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> err</span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;已新增任务：[%d] %s\n&quot;</span>, item.ID, item.Title)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;done&quot;</span>:</span><br><span class="line">        <span class="keyword">if</span> err := service.Done(id); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> err</span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;任务 %d 已标记完成\n&quot;</span>, id)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;不支持的操作: %s&quot;</span>, action)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你会发现这个 <code>main.go</code> 有一个明显特征：</p><p><strong>它几乎不写业务细节，只做四件事：</strong></p><ol><li>解析输入</li><li>初始化依赖</li><li>调用业务服务</li><li>处理边界输出和退出码</li></ol><p>这就是一个很健康的入口结构。</p><hr><h2 id="14-从这个示例里你应该学到什么">14. 从这个示例里你应该学到什么</h2><p>这个小项目虽然不大，但已经有了比较完整的工程轮廓。</p><h3 id="14-1-业务逻辑不放在-main">14.1 业务逻辑不放在 <code>main</code></h3><p><code>main</code> 不是写业务规则的地方。</p><p>如果以后你要：</p><ul><li>写单元测试</li><li>增加 HTTP 接口</li><li>改成 GUI 或 Web 后台</li></ul><p>只要 <code>task.Service</code> 还在，核心逻辑就能复用。</p><h3 id="14-2-存储实现可以替换">14.2 存储实现可以替换</h3><p>今天是 <code>FileStore</code>，明天可以换成 <code>DBStore</code>：</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="keyword">type</span> DBStore <span class="keyword">struct</span> &#123;&#125;</span><br></pre></td></tr></table></figure><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">Load() ([]task.Task, <span class="type">error</span>)</span><br><span class="line">Save([]task.Task) <span class="type">error</span></span><br></pre></td></tr></table></figure><p>就能无缝接进 <code>task.Service</code>。</p><p>这就是接口带来的工程价值。</p><h3 id="14-3-配置、日志、错误处理都有统一入口">14.3 配置、日志、错误处理都有统一入口</h3><p>工程组织不只是目录拆分，更重要的是：</p><ul><li>配置统一加载</li><li>日志统一初始化</li><li>错误统一包装和落地处理</li></ul><p>这三件事做好了，项目维护成本会明显下降。</p><hr><h2 id="15-internal-的价值，再单独讲一次">15. <code>internal</code> 的价值，再单独讲一次</h2><p>很多初学者知道 <code>internal</code>，但不知道它真正的工程意义。</p><h3 id="15-1-它不是“普通目录”，而是语言级边界">15.1 它不是“普通目录”，而是语言级边界</h3><p>这和你随便建个 <code>private/</code> 目录完全不同。</p><p><code>private/</code> 只是命名约定。</p><p><code>internal/</code> 则是 Go 编译器真的会帮你拦住外部导入。</p><p>这意味着：</p><ul><li>你可以放心把内部实现放进去</li><li>外部项目无法依赖你的“脆弱内部细节”</li><li>包边界更稳定</li></ul><h3 id="15-2-什么时候应该放进-internal">15.2 什么时候应该放进 <code>internal</code></h3><p>适合放进 <code>internal</code> 的内容：</p><ul><li>业务服务实现</li><li>存储实现</li><li>配置加载</li><li>日志初始化</li><li>只服务于当前项目的工具代码</li></ul><p>不太适合放进 <code>internal</code> 的内容：</p><ul><li>你明确想开放给外部复用的公共库</li><li>你希望其他项目也能 import 的稳定 API</li></ul><h3 id="15-3-一个经验判断">15.3 一个经验判断</h3><p>如果你在想：</p><blockquote><p>“这段代码以后可能给别人复用吗？”</p></blockquote><p>如果答案是“不确定”或“多半不会”，先放 <code>internal</code> 通常是更稳妥的选择。</p><hr><h2 id="16-应用型项目和库型项目，结构不一样">16. 应用型项目和库型项目，结构不一样</h2><p>这部分很容易混淆，必须单独说清楚。</p><h3 id="16-1-应用型项目">16.1 应用型项目</h3><p>比如：</p><ul><li>CLI 工具</li><li>Web 服务</li><li>后台任务程序</li></ul><p>这类项目的目标是“运行起来提供能力”，常见结构是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cmd + internal</span><br></pre></td></tr></table></figure><h3 id="16-2-库型项目">16.2 库型项目</h3><p>比如：</p><ul><li>你写了一个通用的字符串处理库</li><li>你实现了一个别人可复用的缓存组件</li></ul><p>这类项目的目标是“被别的项目 import”，结构往往更简单：</p><figure class="highlight text"><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">my-lib/</span><br><span class="line">├── go.mod</span><br><span class="line">├── cache.go</span><br><span class="line">├── cache_test.go</span><br><span class="line">└── option.go</span><br></pre></td></tr></table></figure><p>或者：</p><figure class="highlight text"><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">my-lib/</span><br><span class="line">├── go.mod</span><br><span class="line">└── cache/</span><br><span class="line">    ├── cache.go</span><br><span class="line">    └── cache_test.go</span><br></pre></td></tr></table></figure><p>此时未必需要 <code>cmd/</code>，也未必需要 <code>internal/</code>。</p><h3 id="16-3-推荐判断方式">16.3 推荐判断方式</h3><table><thead><tr><th>项目类型</th><th>更常见的结构</th></tr></thead><tbody><tr><td>应用型</td><td><code>cmd + internal</code></td></tr><tr><td>库型</td><td>根包或少量公开包</td></tr><tr><td>混合型</td><td>对外库 + 内部应用入口，按需组合</td></tr></tbody></table><p>所以不要拿“库型项目的结构标准”去要求应用项目，也不要反过来。</p><hr><h2 id="17-测试文件该放哪">17. 测试文件该放哪</h2><p>这一课虽然主讲结构，但测试位置也属于工程组织的一部分。</p><h3 id="17-1-最常见做法：测试和被测代码放一起">17.1 最常见做法：测试和被测代码放一起</h3><p>例如：</p><figure class="highlight text"><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">internal/task/</span><br><span class="line">├── service.go</span><br><span class="line">└── service_test.go</span><br></pre></td></tr></table></figure><p>这是 Go 最常见、最推荐的做法。</p><p>好处：</p><ul><li>测试和实现离得近</li><li>重构时不容易漏</li><li>更符合 Go 社区习惯</li></ul><h3 id="17-2-测试数据放-testdata">17.2 测试数据放 <code>testdata/</code></h3><p>如果测试需要样例文件：</p><figure class="highlight text"><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">internal/store/</span><br><span class="line">├── file_store.go</span><br><span class="line">├── file_store_test.go</span><br><span class="line">└── testdata/</span><br><span class="line">    ├── valid_tasks.json</span><br><span class="line">    └── broken_tasks.json</span><br></pre></td></tr></table></figure><p><code>testdata</code> 是 Go 生态里很常见的约定目录。</p><h3 id="17-3-不建议单独建一个巨大的-tests-目录">17.3 不建议单独建一个巨大的 <code>tests/</code> 目录</h3><p>除非你在做：</p><ul><li>集成测试</li><li>端到端测试</li><li>多模块联调测试</li></ul><p>否则把所有测试都扔进一个总 <code>tests/</code> 目录，通常反而更难维护。</p><hr><h2 id="18-从零搭项目时的推荐步骤">18. 从零搭项目时的推荐步骤</h2><p>如果你以后要自己起一个 Go 小项目，建议按下面顺序来。</p><h3 id="18-1-第一步：先起模块">18.1 第一步：先起模块</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go mod init example.com/myapp</span><br></pre></td></tr></table></figure><h3 id="18-2-第二步：只建必要目录">18.2 第二步：只建必要目录</h3><p>对于一个小型应用型项目，可以先建：</p><figure class="highlight text"><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">myapp/</span><br><span class="line">├── go.mod</span><br><span class="line">├── cmd/</span><br><span class="line">│   └── myapp/</span><br><span class="line">│       └── main.go</span><br><span class="line">└── internal/</span><br><span class="line">    └── ...</span><br></pre></td></tr></table></figure><h3 id="18-3-第三步：先确定核心业务包">18.3 第三步：先确定核心业务包</h3><p>先问自己：</p><ul><li>这个项目最核心的业务对象是什么？</li><li>哪些能力是基础设施？</li></ul><p>比如任务工具：</p><ul><li>核心业务：<code>task</code></li><li>基础设施：<code>config</code>、<code>logger</code>、<code>store</code></li></ul><h3 id="18-4-第四步：把-main-变薄">18.4 第四步：把 <code>main</code> 变薄</h3><p>任何时候只要发现 <code>main.go</code> 开始变胖，就考虑把逻辑往包里挪。</p><h3 id="18-5-第五步：边写边补测试">18.5 第五步：边写边补测试</h3><p>工程化不是最后一口气整理目录，而是边增长边控制结构。</p><p>尤其是：</p><ul><li>核心业务包</li><li>存储实现</li><li>配置解析</li></ul><p>这些都非常值得尽早加测试。</p><hr><h2 id="19-常见坑总结">19. 常见坑总结</h2><h3 id="19-1-一上来照抄“大而全模板”">19.1 一上来照抄“大而全模板”</h3><figure class="highlight text"><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">cmd/</span><br><span class="line">internal/</span><br><span class="line">pkg/</span><br><span class="line">api/</span><br><span class="line">build/</span><br><span class="line">deploy/</span><br><span class="line">hack/</span><br><span class="line">tools/</span><br><span class="line">third_party/</span><br></pre></td></tr></table></figure><p>问题不在这些目录本身，而在于：</p><p><strong>你当前项目根本不需要它们。</strong></p><p>解决思路：</p><ul><li>从最小结构开始</li><li>目录随着需求增长</li></ul><h3 id="19-2-main-go-里写满业务逻辑">19.2 <code>main.go</code> 里写满业务逻辑</h3><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"><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">// 解析参数</span></span><br><span class="line">    <span class="comment">// 验证参数</span></span><br><span class="line">    <span class="comment">// 读配置</span></span><br><span class="line">    <span class="comment">// 查任务</span></span><br><span class="line">    <span class="comment">// 改任务</span></span><br><span class="line">    <span class="comment">// 写文件</span></span><br><span class="line">    <span class="comment">// 输出结果</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这会让：</p><ul><li>测试困难</li><li>复用困难</li><li>修改困难</li></ul><p>正确思路：</p><ul><li><code>main</code> 负责组装</li><li>业务逻辑放进领域包</li></ul><h3 id="19-3-乱建-util-common-helper">19.3 乱建 <code>util/common/helper</code></h3><p>这是工程结构最容易腐化的入口。</p><p>如果你发现某段代码想放进 <code>util</code>，先反问一句：</p><blockquote><p>它到底属于哪个明确职责？</p></blockquote><p>大多数时候，它其实应该归属于某个现有包。</p><h3 id="19-4-包之间互相-import">19.4 包之间互相 import</h3><figure class="highlight text"><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">task -&gt; store</span><br><span class="line">store -&gt; task</span><br></pre></td></tr></table></figure><p>这通常说明职责划分不清。</p><p>修复思路通常有两种：</p><ol><li>抽出更稳定的公共抽象</li><li>把接口挪到真正的使用方</li></ol><h3 id="19-5-配置读取散落各处">19.5 配置读取散落各处</h3><p>坏处：</p><ul><li>不知道配置总入口在哪</li><li>改默认值很麻烦</li><li>测试也难做</li></ul><p>正确做法：</p><ul><li>用一个 <code>Config</code> 结构体统一承载</li><li>在启动阶段一次性加载和校验</li></ul><h3 id="19-6-错误既记录又层层打印">19.6 错误既记录又层层打印</h3><p>例如：</p><ul><li><code>store</code> 打一遍错误日志</li><li><code>service</code> 再打一遍</li><li><code>main</code> 再打一遍</li></ul><p>最后控制台全是重复信息。</p><p>正确做法：</p><ul><li>中间层补充上下文并返回</li><li>最外层决定记录和展示</li></ul><h3 id="19-7-导出太多无关-API">19.7 导出太多无关 API</h3><p>如果一个包对外暴露了很多不必要的标识符：</p><ul><li>使用者不知道该用哪个</li><li>重构时不敢动</li><li>包边界会越来越糊</li></ul><p>原则还是那句：</p><p><strong>默认不导出，只有确实需要给包外访问的才导出。</strong></p><hr><h2 id="20-本课练习">20. 本课练习</h2><h3 id="练习-1：重构第-24-课的命令行项目">练习 1：重构第 24 课的命令行项目</h3><p>把第 24 课的命令行小项目从“单包结构”重构为：</p><ul><li><code>cmd/xxx/main.go</code></li><li><code>internal/config</code></li><li><code>internal/&lt;核心业务包&gt;</code></li><li><code>internal/store</code></li></ul><p>要求：</p><ul><li><code>main</code> 只负责参数解析和依赖组装</li><li>核心业务逻辑移出 <code>main</code></li></ul><hr><h3 id="练习-2：给项目加统一配置入口">练习 2：给项目加统一配置入口</h3><p>为一个已有的小项目增加 <code>Config</code> 结构体，统一承载以下配置：</p><ul><li>数据文件路径</li><li>日志级别</li><li>超时时间</li></ul><p>要求：</p><ul><li>给出默认值</li><li>对关键字段做校验</li><li>业务层不能直接调用 <code>os.Getenv</code></li></ul><hr><h3 id="练习-3：给项目加统一日志入口">练习 3：给项目加统一日志入口</h3><p>用 <code>log/slog</code> 为一个小项目创建统一日志初始化函数，要求：</p><ul><li>支持 <code>DEBUG</code>、<code>INFO</code>、<code>WARN</code>、<code>ERROR</code></li><li>在业务层记录至少 3 条结构化日志</li><li>不再使用 <code>fmt.Println</code> 做调试输出</li></ul><hr><h3 id="练习-4：错误包装练习">练习 4：错误包装练习</h3><p>模拟三层调用：</p><ol><li>文件读取层</li><li>业务处理层</li><li>程序入口层</li></ol><p>要求：</p><ul><li>底层返回原始错误</li><li>中间层用 <code>%w</code> 包装</li><li>最外层用 <code>errors.Is</code> 判断特定错误并输出友好提示</li></ul><hr><h3 id="练习-5：画出你的项目依赖图">练习 5：画出你的项目依赖图</h3><p>任选一个你写过的 Go 小项目，画出包依赖关系图，例如：</p><figure class="highlight text"><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">main -&gt; service -&gt; repository</span><br><span class="line">main -&gt; config</span><br><span class="line">main -&gt; logger</span><br></pre></td></tr></table></figure><p>然后检查：</p><ul><li>有没有双向依赖</li><li>有没有职责不清的包</li><li>有没有 <code>util/common</code> 这类模糊目录</li></ul><hr><h2 id="21-自测题">21. 自测题</h2><h3 id="21-1-概念题">21.1 概念题</h3><ol><li><code>cmd/</code> 目录通常放什么？它和 <code>internal/</code> 的职责差别是什么？</li><li>为什么说 <code>main.go</code> 应该尽量“薄”？</li><li><code>internal</code> 相比普通目录最大的价值是什么？</li><li><code>pkg/</code> 是必须的吗？什么时候适合用，什么时候不适合？</li><li>为什么不推荐滥用 <code>util</code>、<code>common</code>、<code>helper</code> 这类包名？</li><li>配置为什么应该集中加载，而不是在各层到处 <code>os.Getenv</code>？</li><li>错误包装里的 <code>%w</code> 有什么作用？为什么工程里要重视它？</li><li>应用型项目和库型项目在结构上有什么典型区别？</li></ol><h3 id="21-2-代码阅读题">21.2 代码阅读题</h3><p>下面这个结构和代码有几个明显问题，试着找出来：</p><figure class="highlight text"><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">todo-app/</span><br><span class="line">├── main.go</span><br><span class="line">├── util.go</span><br><span class="line">├── task.go</span><br><span class="line">├── store.go</span><br><span class="line">└── logger.go</span><br></pre></td></tr></table></figure><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></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;os&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">    dataFile := os.Getenv(<span class="string">&quot;TODO_DATA_FILE&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> dataFile == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        dataFile = <span class="string">&quot;tasks.json&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tasks, err := LoadTasks(dataFile)</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;load error:&quot;</span>, err)</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="keyword">if</span> err := AddTask(tasks, <span class="string">&quot;学习 Go 工程结构&quot;</span>); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;add error:&quot;</span>, err)</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="keyword">if</span> err := SaveTasks(dataFile, tasks); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;save error:&quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>至少有这些问题：</p><p><strong>问题 1：所有逻辑都堆在 <code>main</code> 包里，没有明确边界。</strong></p><ul><li>配置读取、数据读取、业务处理、输出都混在入口里</li><li>后续很难测试和扩展</li></ul><p><strong>问题 2：目录和文件按“随手拆文件”组织，不是按职责或业务边界组织。</strong></p><ul><li><code>util.go</code> 是典型模糊命名</li><li>看目录结构无法快速判断职责</li></ul><p><strong>问题 3：配置没有集中管理。</strong></p><ul><li>入口直接读环境变量</li><li>如果其他地方也读配置，后面会越来越乱</li></ul><p><strong>问题 4：错误处理只有简单打印，没有错误上下文。</strong></p><ul><li><code>fmt.Println(&quot;load error:&quot;, err)</code> 无法形成清晰错误链</li><li>应该在各层包装上下文，在边界层统一输出</li></ul><p><strong>问题 5：没有统一日志方案。</strong></p><ul><li>直接 <code>fmt.Println</code> 不能控制级别，也没有结构化上下文</li></ul><p>更合理的重构方向是：</p><figure class="highlight text"><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">todo-app/</span><br><span class="line">├── go.mod</span><br><span class="line">├── cmd/</span><br><span class="line">│   └── todo/</span><br><span class="line">│       └── main.go</span><br><span class="line">└── internal/</span><br><span class="line">    ├── config/</span><br><span class="line">    ├── logger/</span><br><span class="line">    ├── store/</span><br><span class="line">    └── task/</span><br></pre></td></tr></table></figure></details><hr><h2 id="22-本课总结">22. 本课总结</h2><p>这一课你真正要带走的，不是某个固定模板，而是工程组织的判断能力。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>结构目标</td><td>职责清晰、依赖单向、便于测试、便于扩展</td></tr><tr><td>常见目录</td><td><code>cmd</code> 放入口，<code>internal</code> 放内部实现，<code>pkg</code> 按需使用</td></tr><tr><td>拆包思路</td><td>小项目可按职责拆，大一些的项目更适合按业务领域收拢</td></tr><tr><td>配置管理</td><td>统一加载、统一校验、统一传递</td></tr><tr><td>日志管理</td><td>统一初始化、统一格式、避免到处乱打</td></tr><tr><td>错误处理</td><td>用 <code>%w</code> 包装上下文，用 <code>errors.Is</code> 做边界判断</td></tr></tbody></table><p>最重要的四件事：</p><ol><li><strong>工程结构不是越复杂越高级，而是越匹配当前规模越好</strong></li><li><strong><code>main</code> 负责组装依赖，业务逻辑应该放进明确的包</strong></li><li><strong>配置、日志、错误处理必须统一入口，否则项目会越来越乱</strong></li><li><strong>先建立清晰边界，再考虑扩展能力，<code>internal</code> 是非常实用的边界工具</strong></li></ol><hr><h2 id="23-下一课预告">23. 下一课预告</h2><p>到这里，阶段五就完整了。你已经掌握了并发、测试、Benchmark 和基础工程组织，接下来要进入 Go 的高级能力。</p><p>下一课：<strong>泛型入门</strong></p><p>会重点讲：</p><ul><li>为什么 Go 直到后期才加入泛型</li><li>类型参数是什么</li><li>约束是什么，怎么写</li><li>泛型函数和泛型结构怎么定义</li><li>泛型适合解决什么问题，不适合解决什么问题</li></ul><p>学完下一课，你会真正进入 Go 语言“能看懂更现代代码”的阶段。</p>]]></content>
    
    
    <summary type="html">Go语言系列33</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 32 课：Benchmark 与性能测试</title>
    <link href="https://yjyrichard.github.io/posts/1a6adeac.html"/>
    <id>https://yjyrichard.github.io/posts/1a6adeac.html</id>
    <published>2026-03-22T15:28:11.746Z</published>
    <updated>2026-03-22T15:40:40.034Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 32 课：Benchmark 与性能测试</h1><blockquote><p>学习定位：这是整套 Go 教程的第 32 课，也是阶段五（并发与工程阶段）的第八课。<br>前置要求：已经完成第 31 课，掌握了 Go 的 <code>testing</code> 包、单元测试和表驱动测试。<br>本课目标：掌握 Go 基准测试的写法，理解 <code>b.N</code> 的工作机制，学会用 <code>go test -bench</code> 运行基准测试，能对比不同实现的性能差异，了解基准测试的常见误区。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>上一课你学会了测试——确认代码&quot;对不对&quot;。这一课解决另一个问题：代码&quot;快不快&quot;。</p><p>你写了两种字符串拼接方式，哪个更快？你把 <code>map</code> 换成了 <code>slice</code>，性能提升了多少？你优化了一个函数，到底快了还是慢了？</p><p>靠猜不行，得用数据说话。</p><p>Go 内置了基准测试（Benchmark）功能。跟单元测试一样，不需要装任何第三方工具，写一个函数、跑一条命令，就能得到精确的性能数据。</p><p>这一课讲清楚以下问题：</p><ul><li>基准测试函数怎么写</li><li><code>b.N</code> 是什么，Go 怎么自动决定跑多少次</li><li>怎么运行基准测试、怎么看结果</li><li>怎么对比两种实现的性能差异</li><li>怎么测内存分配</li><li>基准测试有哪些常见的坑</li></ul><hr><h2 id="2-最简单的基准测试">2. 最简单的基准测试</h2><h3 id="2-1-被测函数">2.1 被测函数</h3><p>沿用上一课的 <code>Reverse</code> 函数：</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="comment">// stringutil/stringutil.go</span></span><br><span class="line"><span class="keyword">package</span> stringutil</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Reverse</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(runes)<span class="number">-1</span>; i &lt; j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">        runes[i], runes[j] = runes[j], runes[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-2-写基准测试">2.2 写基准测试</h3><p>在 <code>stringutil_test.go</code> 中加一个函数：</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"><span class="keyword">package</span> stringutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</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">        Reverse(<span class="string">&quot;hello, world&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就这么简单。跟单元测试对比：</p><table><thead><tr><th></th><th>单元测试</th><th>基准测试</th></tr></thead><tbody><tr><td>函数前缀</td><td><code>Test</code></td><td><code>Benchmark</code></td></tr><tr><td>参数类型</td><td><code>*testing.T</code></td><td><code>*testing.B</code></td></tr><tr><td>核心逻辑</td><td>断言结果对不对</td><td>循环 <code>b.N</code> 次测性能</td></tr></tbody></table><h3 id="2-3-运行基准测试">2.3 运行基准测试</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench .</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight apache"><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="attribute">goos</span>: windows</span><br><span class="line"><span class="attribute">goarch</span>: amd64</span><br><span class="line"><span class="attribute">pkg</span>: stringutil</span><br><span class="line"><span class="attribute">cpu</span>: <span class="number">12</span>th Gen Intel(R) Core(TM) i7-<span class="number">12700</span>H</span><br><span class="line"><span class="attribute">BenchmarkReverse</span>-<span class="number">20</span>    <span class="number">8234567</span>    <span class="number">145</span>.<span class="number">2</span> ns/op</span><br><span class="line"><span class="attribute">PASS</span></span><br><span class="line"><span class="attribute">ok</span>      stringutil    <span class="number">1</span>.<span class="number">345</span>s</span><br></pre></td></tr></table></figure><h3 id="2-4-怎么读结果">2.4 怎么读结果</h3><figure class="highlight tap"><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">BenchmarkReverse-20   <span class="number"> 8234567 </span>   145.2 ns/op</span><br><span class="line">│                │     │          │</span><br><span class="line">│                │     │          └── 每次操作耗时 145.2 纳秒</span><br><span class="line">│                │     └── 总共运行了<span class="number"> 8234567 </span>次</span><br><span class="line">│                └── 使用了<span class="number"> 20 </span>个 CPU 核心</span><br><span class="line">└── 函数名</span><br></pre></td></tr></table></figure><p><strong>核心指标是 <code>ns/op</code></strong>——每次操作耗时多少纳秒。这个数字越小，性能越好。</p><hr><h2 id="3-理解-b-N">3. 理解 b.N</h2><h3 id="3-1-b-N-是什么">3.1 b.N 是什么</h3><p><code>b.N</code> 是 Go 自动决定的循环次数。你不需要手动设置。</p><p>Go 的做法是：先用一个小的 N 跑一遍（比如 1 次），看耗时够不够。不够就加大 N（10、100、1000…），直到总耗时达到一个稳定的基准（默认约 1 秒）。</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">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="comment">// Go 自动调整 b.N</span></span><br><span class="line">    <span class="comment">// 第一轮 b.N 可能是 1</span></span><br><span class="line">    <span class="comment">// 第二轮可能是 100</span></span><br><span class="line">    <span class="comment">// 第三轮可能是 10000</span></span><br><span class="line">    <span class="comment">// ...直到总耗时足够稳定</span></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">        Reverse(<span class="string">&quot;hello, world&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-为什么要这样">3.2 为什么要这样</h3><p>如果固定跑 100 次，对于一个耗时 1 微秒的函数来说太少了，误差很大。对于一个耗时 1 秒的函数来说太多了，要等好几分钟。</p><p>Go 自动调整 N，确保：</p><ul><li>耗时短的函数多跑几轮，减小误差</li><li>耗时长的函数少跑几轮，节省时间</li></ul><h3 id="3-3-绝对不要这样写">3.3 绝对不要这样写</h3><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="comment">// 大错特错！</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10000</span>; i++ &#123;  <span class="comment">// 不要写固定数字！</span></span><br><span class="line">        Reverse(<span class="string">&quot;hello, world&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你用固定数字而不是 <code>b.N</code>，Go 会认为&quot;这个函数只需要跑一次&quot;，然后给你一个没有意义的结果。</p><hr><h2 id="4-运行基准测试的常用命令">4. 运行基准测试的常用命令</h2><h3 id="4-1-基本命令">4.1 基本命令</h3><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 运行当前包的所有基准测试（不运行单元测试）</span></span><br><span class="line">go <span class="built_in">test</span> -bench .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行所有包的基准测试</span></span><br><span class="line">go <span class="built_in">test</span> -bench . ./...</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行指定名称的基准测试（支持正则）</span></span><br><span class="line">go <span class="built_in">test</span> -bench BenchmarkReverse</span><br><span class="line">go <span class="built_in">test</span> -bench <span class="string">&quot;Benchmark(Reverse|Concat)&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 同时显示内存分配信息</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchmem</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定每个基准测试运行的时间（默认 1 秒）</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchtime 3s</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定每个基准测试运行固定次数</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchtime 100x</span><br><span class="line"></span><br><span class="line"><span class="comment"># 多次运行以获得更稳定的结果</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -count 5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 跳过单元测试（只跑基准测试）</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -run ^$</span><br></pre></td></tr></table></figure><h3 id="4-2-最常用的组合">4.2 最常用的组合</h3><figure class="highlight bash"><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"><span class="comment"># 开发时对比性能：显示内存 + 跑 3 次取平均</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchmem -count 3</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只跑某个基准测试，排除单元测试干扰</span></span><br><span class="line">go <span class="built_in">test</span> -bench BenchmarkReverse -run ^$ -benchmem</span><br><span class="line"></span><br><span class="line"><span class="comment"># 跑久一点，结果更稳定</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchtime 5s -benchmem</span><br></pre></td></tr></table></figure><h3 id="4-3-关于-run">4.3 关于 -run ^$</h3><p><code>go test -bench .</code> 默认会同时运行单元测试。如果你只想跑基准测试，加 <code>-run ^$</code>（匹配一个不存在的测试名），这样单元测试全部被跳过。</p><hr><h2 id="5-对比不同实现的性能">5. 对比不同实现的性能</h2><p>基准测试最有价值的场景就是<strong>对比</strong>。</p><h3 id="5-1-字符串拼接的三种方式">5.1 字符串拼接的三种方式</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> stringutil</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;strings&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式 1：用 + 号拼接</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ConcatPlus</span><span class="params">(strs []<span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    result := <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> strs &#123;</span><br><span class="line">        result += s</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式 2：用 fmt.Sprintf</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ConcatSprintf</span><span class="params">(strs []<span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    result := <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> strs &#123;</span><br><span class="line">        result = fmt.Sprintf(<span class="string">&quot;%s%s&quot;</span>, result, s)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式 3：用 strings.Builder</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ConcatBuilder</span><span class="params">(strs []<span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">var</span> builder strings.Builder</span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> strs &#123;</span><br><span class="line">        builder.WriteString(s)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> builder.String()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式 4：用 strings.Join</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ConcatJoin</span><span class="params">(strs []<span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> strings.Join(strs, <span class="string">&quot;&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-2-写基准测试">5.2 写基准测试</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> stringutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;strings&quot;</span></span><br><span class="line">    <span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 准备测试数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">makeStrings</span><span class="params">(n <span class="type">int</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line">    strs := <span class="built_in">make</span>([]<span class="type">string</span>, n)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> strs &#123;</span><br><span class="line">        strs[i] = <span class="string">&quot;hello&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> strs</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">BenchmarkConcatPlus</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    strs := makeStrings(<span class="number">100</span>)</span><br><span class="line">    b.ResetTimer()  <span class="comment">// 重置计时器，排除准备数据的时间</span></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">        ConcatPlus(strs)</span><br><span class="line">    &#125;</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">BenchmarkConcatSprintf</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    strs := makeStrings(<span class="number">100</span>)</span><br><span class="line">    b.ResetTimer()</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">        ConcatSprintf(strs)</span><br><span class="line">    &#125;</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">BenchmarkConcatBuilder</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    strs := makeStrings(<span class="number">100</span>)</span><br><span class="line">    b.ResetTimer()</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">        ConcatBuilder(strs)</span><br><span class="line">    &#125;</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">BenchmarkConcatJoin</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    strs := makeStrings(<span class="number">100</span>)</span><br><span class="line">    b.ResetTimer()</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">        ConcatJoin(strs)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-运行并对比">5.3 运行并对比</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench BenchmarkConcat -benchmem -run ^$</span><br></pre></td></tr></table></figure><p>输出（示意）：</p><figure class="highlight apache"><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="attribute">BenchmarkConcatPlus</span>-<span class="number">20</span>       <span class="number">18534</span>    <span class="number">64521</span> ns/op    <span class="number">53336</span> B/op    <span class="number">99</span> allocs/op</span><br><span class="line"><span class="attribute">BenchmarkConcatSprintf</span>-<span class="number">20</span>     <span class="number">8245</span>   <span class="number">145234</span> ns/op   <span class="number">106824</span> B/op   <span class="number">298</span> allocs/op</span><br><span class="line"><span class="attribute">BenchmarkConcatBuilder</span>-<span class="number">20</span>   <span class="number">325678</span>     <span class="number">3682</span> ns/op     <span class="number">2544</span> B/op     <span class="number">8</span> allocs/op</span><br><span class="line"><span class="attribute">BenchmarkConcatJoin</span>-<span class="number">20</span>      <span class="number">412345</span>     <span class="number">2916</span> ns/op      <span class="number">512</span> B/op     <span class="number">1</span> allocs/op</span><br></pre></td></tr></table></figure><h3 id="5-4-怎么读对比结果">5.4 怎么读对比结果</h3><figure class="highlight tap"><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><br><span class="line">ConcatPlus      <span class="number"> 18534 </span>  <span class="number"> 64521 </span>ns/op   <span class="number"> 53336 </span>B/op   <span class="number"> 99 </span>allocs/op</span><br><span class="line">ConcatSprintf    <span class="number"> 8245 </span> <span class="number"> 145234 </span>ns/op  <span class="number"> 106824 </span>B/op  <span class="number"> 298 </span>allocs/op</span><br><span class="line">ConcatBuilder  <span class="number"> 325678 </span>   <span class="number"> 3682 </span>ns/op    <span class="number"> 2544 </span>B/op    <span class="number"> 8 </span>allocs/op</span><br><span class="line">ConcatJoin     <span class="number"> 412345 </span>   <span class="number"> 2916 </span>ns/op     <span class="number"> 512 </span>B/op    <span class="number"> 1 </span>allocs/op</span><br></pre></td></tr></table></figure><table><thead><tr><th>指标</th><th>含义</th></tr></thead><tbody><tr><td><code>ns/op</code></td><td>每次操作耗时（纳秒）。越小越快</td></tr><tr><td><code>B/op</code></td><td>每次操作分配的内存（字节）。越小越省内存</td></tr><tr><td><code>allocs/op</code></td><td>每次操作的内存分配次数。越少越好</td></tr></tbody></table><p>结论一目了然：</p><ul><li><code>strings.Join</code> 最快，内存分配最少</li><li><code>strings.Builder</code> 紧随其后</li><li><code>+</code> 号拼接慢了 20 倍，内存分配多了 100 倍</li><li><code>fmt.Sprintf</code> 最慢</li></ul><p><strong>这就是基准测试的价值——用数据说话，不靠猜测。</strong></p><hr><h2 id="6-b-ResetTimer、b-StopTimer、b-StartTimer">6. b.ResetTimer、b.StopTimer、b.StartTimer</h2><h3 id="6-1-b-ResetTimer">6.1 b.ResetTimer()</h3><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"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkProcess</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 准备工作：加载数据、初始化等</span></span><br><span class="line">    data := loadLargeFile()  <span class="comment">// 可能耗时 2 秒</span></span><br><span class="line"></span><br><span class="line">    b.ResetTimer()  <span class="comment">// 从这里开始计时</span></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">        process(data)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不加 <code>b.ResetTimer()</code>，2 秒的准备时间会被算进基准测试结果，导致数据不准。</p><h3 id="6-2-b-StopTimer-和-b-StartTimer">6.2 b.StopTimer() 和 b.StartTimer()</h3><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><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">BenchmarkInsert</span><span class="params">(b *testing.B)</span></span> &#123;</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">        b.StopTimer()</span><br><span class="line">        db := setupTestDB()   <span class="comment">// 每次循环都需要准备，但不想计入耗时</span></span><br><span class="line">        b.StartTimer()</span><br><span class="line"></span><br><span class="line">        db.Insert(<span class="string">&quot;key&quot;</span>, <span class="string">&quot;value&quot;</span>)  <span class="comment">// 只测这一行的性能</span></span><br><span class="line"></span><br><span class="line">        b.StopTimer()</span><br><span class="line">        db.Cleanup()  <span class="comment">// 清理也不计入</span></span><br><span class="line">        b.StartTimer()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：<code>StopTimer/StartTimer</code> 本身有开销。如果被测代码执行很快（几十纳秒），频繁调用它们会导致结果不准确。能用 <code>b.ResetTimer()</code> 解决的就别用 <code>StopTimer/StartTimer</code>。</p><h3 id="6-3-选择建议">6.3 选择建议</h3><table><thead><tr><th>场景</th><th>用什么</th></tr></thead><tbody><tr><td>循环前有一次性准备工作</td><td><code>b.ResetTimer()</code></td></tr><tr><td>每次循环内部有准备/清理工作</td><td><code>b.StopTimer()</code> + <code>b.StartTimer()</code></td></tr><tr><td>没有额外准备工作</td><td>什么都不用加</td></tr></tbody></table><hr><h2 id="7-内存分配基准测试">7. 内存分配基准测试</h2><h3 id="7-1-用-benchmem-看内存">7.1 用 -benchmem 看内存</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -benchmem</span><br></pre></td></tr></table></figure><p>输出中会多两列：</p><figure class="highlight tap"><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">BenchmarkReverse-20   <span class="number"> 8234567 </span>   145.2 ns/op   <span class="number"> 48 </span>B/op   <span class="number"> 2 </span>allocs/op</span><br><span class="line">                                                  │          │</span><br><span class="line">                                                  │          └── 每次操作分配了<span class="number"> 2 </span>次内存</span><br><span class="line">                                                  └── 每次操作分配了<span class="number"> 48 </span>字节</span><br></pre></td></tr></table></figure><h3 id="7-2-用-b-ReportAllocs">7.2 用 b.ReportAllocs()</h3><p>也可以在代码中强制报告内存，不需要加 <code>-benchmem</code> 参数：</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="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    b.ReportAllocs()  <span class="comment">// 强制报告内存分配</span></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">        Reverse(<span class="string">&quot;hello, world&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-3-为什么要关注内存分配">7.3 为什么要关注内存分配</h3><p>Go 的垃圾回收器（GC）需要处理所有分配的内存。分配越多：</p><ul><li>GC 压力越大</li><li>程序可能出现短暂停顿</li><li>总体性能下降</li></ul><p>很多性能优化的核心就是<strong>减少内存分配</strong>。</p><h3 id="7-4-实例：减少内存分配的优化">7.4 实例：减少内存分配的优化</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 优化前：每次调用都分配新的 []rune</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReverseV1</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(runes)<span class="number">-1</span>; i &lt; j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">        runes[i], runes[j] = runes[j], runes[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 优化后：纯 ASCII 字符串走快速路径，避免 []rune 转换</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReverseV2</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="comment">// 快速路径：纯 ASCII</span></span><br><span class="line">    isASCII := <span class="literal">true</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> s[i] &gt; <span class="number">127</span> &#123;</span><br><span class="line">            isASCII = <span class="literal">false</span></span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> isASCII &#123;</span><br><span class="line">        b := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="built_in">len</span>(s))</span><br><span class="line">        <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(s)<span class="number">-1</span>; i &lt;= j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">            b[i], b[j] = s[j], s[i]</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="type">string</span>(b)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 慢速路径：有非 ASCII 字符</span></span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(runes)<span class="number">-1</span>; i &lt; j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">        runes[i], runes[j] = runes[j], runes[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverseV1</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    b.ReportAllocs()</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">        ReverseV1(<span class="string">&quot;hello, world!&quot;</span>)</span><br><span class="line">    &#125;</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">BenchmarkReverseV2</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    b.ReportAllocs()</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">        ReverseV2(<span class="string">&quot;hello, world!&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>结果（示意）：</p><figure class="highlight apache"><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="attribute">BenchmarkReverseV1</span>-<span class="number">20</span>    <span class="number">5432198</span>    <span class="number">221</span>.<span class="number">3</span> ns/op    <span class="number">80</span> B/op    <span class="number">2</span> allocs/op</span><br><span class="line"><span class="attribute">BenchmarkReverseV2</span>-<span class="number">20</span>    <span class="number">9876543</span>    <span class="number">121</span>.<span class="number">5</span> ns/op    <span class="number">32</span> B/op    <span class="number">1</span> allocs/op</span><br></pre></td></tr></table></figure><p>V2 对纯 ASCII 字符串快了近一倍，内存分配也少了一半。</p><hr><h2 id="8-子基准测试">8. 子基准测试</h2><h3 id="8-1-用-b-Run-测试不同规模">8.1 用 b.Run 测试不同规模</h3><p>跟单元测试的 <code>t.Run</code> 一样，基准测试也支持 <code>b.Run</code> 创建子基准测试。最常见的用法是<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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    sizes := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name <span class="type">string</span></span><br><span class="line">        input <span class="type">string</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;短字符串&quot;</span>, <span class="string">&quot;hello&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;中等字符串&quot;</span>, <span class="string">&quot;hello, world! this is a benchmark test&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;长字符串&quot;</span>, strings.Repeat(<span class="string">&quot;abcdefghij&quot;</span>, <span class="number">100</span>)&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> sizes &#123;</span><br><span class="line">        b.Run(s.name, <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                Reverse(s.input)</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></pre></td></tr></table></figure><p>输出：</p><figure class="highlight apache"><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="attribute">BenchmarkReverse</span>/短字符串-<span class="number">20</span>      <span class="number">12345678</span>    <span class="number">97</span>.<span class="number">3</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkReverse</span>/中等字符串-<span class="number">20</span>     <span class="number">4567890</span>   <span class="number">263</span>.<span class="number">1</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkReverse</span>/长字符串-<span class="number">20</span>        <span class="number">123456</span>  <span class="number">9712</span>.<span class="number">0</span> ns/op</span><br></pre></td></tr></table></figure><p>一眼看出性能跟输入长度的关系。</p><h3 id="8-2-用-b-Run-对比多种实现">8.2 用 b.Run 对比多种实现</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkConcat</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    strs := makeStrings(<span class="number">100</span>)</span><br><span class="line"></span><br><span class="line">    b.Run(<span class="string">&quot;Plus&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">            ConcatPlus(strs)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    b.Run(<span class="string">&quot;Builder&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">            ConcatBuilder(strs)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    b.Run(<span class="string">&quot;Join&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">            ConcatJoin(strs)</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>输出：</p><figure class="highlight apache"><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="attribute">BenchmarkConcat</span>/Plus-<span class="number">20</span>        <span class="number">18534</span>    <span class="number">64521</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcat</span>/Builder-<span class="number">20</span>    <span class="number">325678</span>     <span class="number">3682</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcat</span>/Join-<span class="number">20</span>       <span class="number">412345</span>     <span class="number">2916</span> ns/op</span><br></pre></td></tr></table></figure><h3 id="8-3-综合：不同实现-×-不同规模">8.3 综合：不同实现 × 不同规模</h3><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="function"><span class="keyword">func</span> <span class="title">BenchmarkConcatMatrix</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    sizes := []<span class="type">int</span>&#123;<span class="number">10</span>, <span class="number">100</span>, <span class="number">1000</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, size := <span class="keyword">range</span> sizes &#123;</span><br><span class="line">        strs := makeStrings(size)</span><br><span class="line"></span><br><span class="line">        b.Run(fmt.Sprintf(<span class="string">&quot;Plus/%d&quot;</span>, size), <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                ConcatPlus(strs)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">        b.Run(fmt.Sprintf(<span class="string">&quot;Builder/%d&quot;</span>, size), <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                ConcatBuilder(strs)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">        b.Run(fmt.Sprintf(<span class="string">&quot;Join/%d&quot;</span>, size), <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                ConcatJoin(strs)</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></pre></td></tr></table></figure><p>输出（示意）：</p><figure class="highlight apache"><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"><span class="attribute">BenchmarkConcatMatrix</span>/Plus/<span class="number">10</span>-<span class="number">20</span>       <span class="number">567890</span>      <span class="number">2105</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Builder/<span class="number">10</span>-<span class="number">20</span>   <span class="number">2345678</span>       <span class="number">511</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Join/<span class="number">10</span>-<span class="number">20</span>      <span class="number">3456789</span>       <span class="number">345</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Plus/<span class="number">100</span>-<span class="number">20</span>       <span class="number">18534</span>     <span class="number">64521</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Builder/<span class="number">100</span>-<span class="number">20</span>   <span class="number">325678</span>      <span class="number">3682</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Join/<span class="number">100</span>-<span class="number">20</span>      <span class="number">412345</span>      <span class="number">2916</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Plus/<span class="number">1000</span>-<span class="number">20</span>       <span class="number">1234</span>   <span class="number">9654321</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Builder/<span class="number">1000</span>-<span class="number">20</span>   <span class="number">45678</span>     <span class="number">26345</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkConcatMatrix</span>/Join/<span class="number">1000</span>-<span class="number">20</span>      <span class="number">56789</span>     <span class="number">21234</span> ns/op</span><br></pre></td></tr></table></figure><p>可以看到：<code>+</code> 号拼接在数据量增大时性能急剧下降（O(n²)），而 <code>Builder</code> 和 <code>Join</code> 基本线性增长。</p><hr><h2 id="9-完整实战：map-vs-slice-查找性能">9. 完整实战：map vs slice 查找性能</h2><h3 id="9-1-两种查找方式">9.1 两种查找方式</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> lookup</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在 slice 中线性查找</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceLookup</span><span class="params">(data []<span class="type">int</span>, target <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, v := <span class="keyword">range</span> data &#123;</span><br><span class="line">        <span class="keyword">if</span> v == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在 map 中直接查找</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">MapLookup</span><span class="params">(data <span class="keyword">map</span>[<span class="type">int</span>]<span class="type">bool</span>, target <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> data[target]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-基准测试">9.2 基准测试</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> lookup</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><br><span class="line"></span><br><span class="line"><span class="comment">// 准备 slice 数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">makeSlice</span><span class="params">(n <span class="type">int</span>)</span></span> []<span class="type">int</span> &#123;</span><br><span class="line">    s := <span class="built_in">make</span>([]<span class="type">int</span>, n)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> s &#123;</span><br><span class="line">        s[i] = i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 准备 map 数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">makeMap</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="keyword">map</span>[<span class="type">int</span>]<span class="type">bool</span> &#123;</span><br><span class="line">    m := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">int</span>]<span class="type">bool</span>, n)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line">        m[i] = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> m</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">BenchmarkLookup</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    sizes := []<span class="type">int</span>&#123;<span class="number">10</span>, <span class="number">100</span>, <span class="number">1000</span>, <span class="number">10000</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, size := <span class="keyword">range</span> sizes &#123;</span><br><span class="line">        sliceData := makeSlice(size)</span><br><span class="line">        mapData := makeMap(size)</span><br><span class="line">        target := size - <span class="number">1</span>  <span class="comment">// 查找最后一个元素（最坏情况）</span></span><br><span class="line"></span><br><span class="line">        b.Run(fmt.Sprintf(<span class="string">&quot;Slice/%d&quot;</span>, size), <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                SliceLookup(sliceData, target)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;)</span><br><span class="line"></span><br><span class="line">        b.Run(fmt.Sprintf(<span class="string">&quot;Map/%d&quot;</span>, size), <span class="function"><span class="keyword">func</span><span class="params">(b *testing.B)</span></span> &#123;</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">                MapLookup(mapData, target)</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></pre></td></tr></table></figure><h3 id="9-3-预期结果分析">9.3 预期结果分析</h3><figure class="highlight apache"><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"><span class="attribute">BenchmarkLookup</span>/Slice/<span class="number">10</span>-<span class="number">20</span>       <span class="number">50000000</span>     <span class="number">24</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Map/<span class="number">10</span>-<span class="number">20</span>         <span class="number">30000000</span>     <span class="number">40</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Slice/<span class="number">100</span>-<span class="number">20</span>       <span class="number">5000000</span>    <span class="number">288</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Map/<span class="number">100</span>-<span class="number">20</span>        <span class="number">25000000</span>     <span class="number">45</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Slice/<span class="number">1000</span>-<span class="number">20</span>       <span class="number">500000</span>   <span class="number">2876</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Map/<span class="number">1000</span>-<span class="number">20</span>       <span class="number">20000000</span>     <span class="number">51</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Slice/<span class="number">10000</span>-<span class="number">20</span>       <span class="number">50000</span>  <span class="number">28934</span> ns/op</span><br><span class="line"><span class="attribute">BenchmarkLookup</span>/Map/<span class="number">10000</span>-<span class="number">20</span>      <span class="number">18000000</span>     <span class="number">58</span> ns/op</span><br></pre></td></tr></table></figure><p>结论：</p><ul><li><strong>数据量小（10 个以下）</strong>：slice 比 map 快（没有 hash 计算的开销）</li><li><strong>数据量大（100+）</strong>：map 远快于 slice（O(1) vs O(n)）</li><li><strong>拐点大约在 20~30 个元素</strong></li></ul><p>这种数据对日常决策很有用：<strong>小集合用 slice，大集合用 map。</strong></p><hr><h2 id="10-常见坑总结">10. 常见坑总结</h2><h3 id="10-1-编译器优化掉了你的代码">10.1 编译器优化掉了你的代码</h3><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：结果没有被使用，编译器可能直接优化掉整个函数调用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</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">        Reverse(<span class="string">&quot;hello&quot;</span>)  <span class="comment">// 返回值被丢弃，编译器可能跳过调用</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：用一个包级变量&quot;消费&quot;结果</span></span><br><span class="line"><span class="keyword">var</span> result <span class="type">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> r <span class="type">string</span></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">        r = Reverse(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    result = r  <span class="comment">// 防止编译器优化</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Go 编译器很聪明。如果它发现一个函数的返回值没被使用，在某些情况下会优化掉整个调用。把结果赋给包级变量可以阻止这种优化。</p><p><strong>实际中</strong>，Go 目前的编译器在大多数情况下不会优化掉有副作用的函数调用。但养成这个习惯是好的，尤其是测试纯计算函数时。</p><h3 id="10-2-循环内部做了太多不相关的事">10.2 循环内部做了太多不相关的事</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：每次循环都重新准备数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkProcess</span><span class="params">(b *testing.B)</span></span> &#123;</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">        data := loadData()    <span class="comment">// 这个耗时被算进去了！</span></span><br><span class="line">        Process(data)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：数据准备放循环外面</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkProcess</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    data := loadData()</span><br><span class="line">    b.ResetTimer()</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">        Process(data)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-3-忘了-b-ResetTimer">10.3 忘了 b.ResetTimer()</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：初始化耗时被计入</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkDB</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    db := connectDatabase()     <span class="comment">// 可能耗时 500ms</span></span><br><span class="line">    <span class="keyword">defer</span> db.Close()</span><br><span class="line">    <span class="comment">// 没有 ResetTimer，500ms 被算进结果</span></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">        db.Query(<span class="string">&quot;SELECT 1&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkDB</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    db := connectDatabase()</span><br><span class="line">    <span class="keyword">defer</span> db.Close()</span><br><span class="line">    b.ResetTimer()  <span class="comment">// 从这里开始计时</span></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">        db.Query(<span class="string">&quot;SELECT 1&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-4-用固定数字代替-b-N">10.4 用固定数字代替 b.N</h3><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="comment">// 大错：不用 b.N</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReverse</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10000</span>; i++ &#123;</span><br><span class="line">        Reverse(<span class="string">&quot;hello&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面已经说过，不再重复。记住：<strong>循环条件永远是 <code>i &lt; b.N</code>。</strong></p><h3 id="10-5-基准测试结果不稳定">10.5 基准测试结果不稳定</h3><p>同一个基准测试，每次跑出来的结果可能不一样。这是正常的。减小波动的方法：</p><figure class="highlight bash"><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="comment"># 方法 1：多次运行取平均</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -count 5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方法 2：增加运行时间</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchtime 5s</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方法 3：关闭不必要的后台程序，减少系统干扰</span></span><br></pre></td></tr></table></figure><p><strong>不要在做其他事情的电脑上跑基准测试</strong>。浏览器、音乐播放器、IDE 都会影响结果。</p><h3 id="10-6-只看-ns-op，忽略-allocs-op">10.6 只看 ns/op，忽略 allocs/op</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -benchmem</span><br></pre></td></tr></table></figure><p>有时候两种实现的 <code>ns/op</code> 差不多，但 <code>allocs/op</code> 差很多。内存分配多的在高并发场景下会因 GC 压力变慢。<strong>永远加 <code>-benchmem</code>。</strong></p><hr><h2 id="11-实用技巧">11. 实用技巧</h2><h3 id="11-1-benchstat-工具">11.1 benchstat 工具</h3><p>Go 官方提供了 <code>benchstat</code> 工具，用于<strong>统计分析</strong>基准测试结果：</p><figure class="highlight bash"><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="comment"># 安装</span></span><br><span class="line">go install golang.org/x/perf/cmd/benchstat@latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用：先把结果保存到文件</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchmem -count 5 &gt; old.txt</span><br><span class="line"></span><br><span class="line"><span class="comment"># 优化代码后再跑一次</span></span><br><span class="line">go <span class="built_in">test</span> -bench . -benchmem -count 5 &gt; new.txt</span><br><span class="line"></span><br><span class="line"><span class="comment"># 对比</span></span><br><span class="line">benchstat old.txt new.txt</span><br></pre></td></tr></table></figure><p>输出（示意）：</p><figure class="highlight apache"><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"><span class="attribute">name</span>          old time/op    new time/op    delta</span><br><span class="line"><span class="attribute">Reverse</span>-<span class="number">20</span>    <span class="number">145</span>ns ± <span class="number">2</span>%     <span class="number">122</span>ns ± <span class="number">1</span>%    -<span class="number">15</span>.<span class="number">86</span>%  (p=<span class="number">0</span>.<span class="number">008</span> n=<span class="number">5</span>+<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="attribute">name</span>          old alloc/op   new alloc/op   delta</span><br><span class="line"><span class="attribute">Reverse</span>-<span class="number">20</span>    <span class="number">48</span>.<span class="number">0</span>B ± <span class="number">0</span>%     <span class="number">32</span>.<span class="number">0</span>B ± <span class="number">0</span>%    -<span class="number">33</span>.<span class="number">33</span>%  (p=<span class="number">0</span>.<span class="number">008</span> n=<span class="number">5</span>+<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line"><span class="attribute">name</span>          old allocs/op  new allocs/op  delta</span><br><span class="line"><span class="attribute">Reverse</span>-<span class="number">20</span>     <span class="number">2</span>.<span class="number">00</span> ± <span class="number">0</span>%      <span class="number">1</span>.<span class="number">00</span> ± <span class="number">0</span>%    -<span class="number">50</span>.<span class="number">00</span>%  (p=<span class="number">0</span>.<span class="number">008</span> n=<span class="number">5</span>+<span class="number">5</span>)</span><br></pre></td></tr></table></figure><p><code>benchstat</code> 会计算标准差、p 值，告诉你性能变化是否<strong>统计显著</strong>。<code>±2%</code> 是波动范围，<code>p=0.008</code> 说明变化是真实的（不是噪音）。</p><h3 id="11-2-b-ReportMetric-自定义指标">11.2 b.ReportMetric() 自定义指标</h3><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">BenchmarkSort</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    data := makeRandomSlice(<span class="number">10000</span>)</span><br><span class="line">    b.ResetTimer()</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">        sorted := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="built_in">len</span>(data))</span><br><span class="line">        <span class="built_in">copy</span>(sorted, data)</span><br><span class="line">        sort.Ints(sorted)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 报告自定义指标</span></span><br><span class="line">    b.ReportMetric(<span class="type">float64</span>(<span class="built_in">len</span>(data)), <span class="string">&quot;items/op&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出会多一列 <code>10000 items/op</code>，让你知道每次操作处理了多少数据。</p><h3 id="11-3-testing-Short-跳过慢的基准测试">11.3 testing.Short() 跳过慢的基准测试</h3><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="function"><span class="keyword">func</span> <span class="title">BenchmarkExpensive</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> testing.Short() &#123;</span><br><span class="line">        b.Skip(<span class="string">&quot;跳过耗时基准测试&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -bench . -short  <span class="comment"># 跳过标记了 Short 的基准测试</span></span><br></pre></td></tr></table></figure><hr><h2 id="12-本课练习">12. 本课练习</h2><h3 id="练习-1：切片追加的性能">练习 1：切片追加的性能</h3><p>对比以下三种构建切片的方式：</p><ol><li>直接 <code>append</code>（不预分配容量）</li><li><code>make([]int, 0, n)</code> 预分配容量后 <code>append</code></li><li><code>make([]int, n)</code> 后用索引赋值</li></ol><p>用基准测试对比它们在 n=100、n=1000、n=10000 时的性能差异。</p><hr><h3 id="练习-2：map-初始化的影响">练习 2：map 初始化的影响</h3><p>对比以下两种 map 使用方式：</p><ol><li><code>make(map[string]int)</code> 不指定容量</li><li><code>make(map[string]int, n)</code> 预分配容量</li></ol><p>向 map 中插入 1000 个键值对，用基准测试对比性能和内存分配。</p><hr><h3 id="练习-3：字符串查找">练习 3：字符串查找</h3><p>对比以下几种判断字符串是否包含子串的方式：</p><ol><li><code>strings.Contains</code></li><li><code>strings.Index &gt;= 0</code></li><li>手写循环匹配</li></ol><p>在短字符串（20 字符）和长字符串（10000 字符）上分别测试。</p><hr><h3 id="练习-4：JSON-序列化">练习 4：JSON 序列化</h3><p>创建一个包含 10 个字段的结构体，用基准测试对比：</p><ol><li><code>json.Marshal</code></li><li>手动拼接 JSON 字符串</li></ol><p>测量性能差异和内存分配差异。</p><hr><h3 id="练习-5：完整的优化流程">练习 5：完整的优化流程</h3><ol><li>写一个函数：接收一个字符串切片，返回所有字符串的去重结果</li><li>先写最简单的实现（两层循环）</li><li>再写优化实现（用 map 去重）</li><li>用基准测试对比两种实现</li><li>用 <code>benchstat</code> 生成对比报告</li></ol><hr><h2 id="13-自测题">13. 自测题</h2><h3 id="13-1-概念题">13.1 概念题</h3><ol><li>基准测试函数的命名规则和参数类型是什么？</li><li><code>b.N</code> 是什么？为什么不能用固定数字代替它？</li><li><code>b.ResetTimer()</code> 的作用是什么？什么时候需要用？</li><li><code>-benchmem</code> 参数输出的三列数据分别是什么意思？</li><li>为什么说基准测试中编译器优化是一个坑？怎么避免？</li><li><code>b.Run</code> 创建子基准测试有什么好处？</li><li>什么时候应该用 <code>b.StopTimer/b.StartTimer</code>？有什么注意事项？</li><li>为什么基准测试结果每次都不完全一样？怎么让结果更可靠？</li></ol><h3 id="13-2-代码阅读题">13.2 代码阅读题</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkProcess</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    data := loadLargeDataset()  <span class="comment">// 耗时 3 秒</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">10000</span>; i++ &#123;</span><br><span class="line">        result := Process(data)</span><br><span class="line">        fmt.Println(result)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p><strong>问题 1：用了固定数字 10000 而不是 <code>b.N</code></strong>。循环条件应该是 <code>i &lt; b.N</code>，否则 Go 无法正确测量每次操作的耗时。</p><p><strong>问题 2：没有用 <code>b.ResetTimer()</code></strong>。<code>loadLargeDataset()</code> 耗时 3 秒，这个时间会被计入基准测试结果，导致 <code>ns/op</code> 严重偏高。</p><p>还有一个额外问题：<code>fmt.Println(result)</code> 会做 I/O 操作，这个耗时也被算进去了。如果目的是防止编译器优化，应该赋给包级变量，而不是打印。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> benchResult <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkProcess</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    data := loadLargeDataset()</span><br><span class="line">    b.ResetTimer()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> r <span class="type">int</span></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">        r = Process(data)</span><br><span class="line">    &#125;</span><br><span class="line">    benchResult = r</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></details><hr><h2 id="14-本课总结">14. 本课总结</h2><p>这一课你学到了 Go 基准测试的核心内容。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>基本写法</td><td><code>BenchmarkXxx(b *testing.B)</code>，循环 <code>b.N</code> 次</td></tr><tr><td>运行命令</td><td><code>go test -bench . -benchmem</code></td></tr><tr><td>核心指标</td><td><code>ns/op</code>（耗时）、<code>B/op</code>（内存）、<code>allocs/op</code>（分配次数）</td></tr><tr><td>计时控制</td><td><code>b.ResetTimer()</code>、<code>b.StopTimer()</code>、<code>b.StartTimer()</code></td></tr><tr><td>子基准测试</td><td><code>b.Run</code> 对比不同规模、不同实现</td></tr><tr><td>分析工具</td><td><code>benchstat</code> 统计对比、<code>-count</code> 多次运行</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>循环条件永远是 <code>i &lt; b.N</code>——Go 自动调整 N 来获得稳定结果</strong></li><li><strong>永远加 <code>-benchmem</code>——内存分配往往比执行时间更能说明问题</strong></li><li><strong>优化前先跑基准测试，优化后再跑一次对比——用数据驱动决策，不靠猜测</strong></li></ol><hr><h2 id="15-下一课预告">15. 下一课预告</h2><p>到这里你已经会写测试、会跑基准测试了。下一步该学怎么组织一个完整的 Go 项目。</p><p>下一课：<strong>项目结构与工程组织</strong></p><p>会重点讲：</p><ul><li>Go 项目的常见目录结构</li><li>包怎么拆分才合理</li><li>配置、日志和错误包装的标准做法</li><li><code>internal</code> 目录的用途</li><li>从零搭建一个结构清晰的小型工程项目</li></ul><p>学完下一课，你就不再是&quot;能写 Go 代码&quot;，而是&quot;能组织 Go 项目&quot;了。</p>]]></content>
    
    
    <summary type="html">Go语言系列32</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 31 课：测试基础</title>
    <link href="https://yjyrichard.github.io/posts/cab9b418.html"/>
    <id>https://yjyrichard.github.io/posts/cab9b418.html</id>
    <published>2026-03-22T15:28:11.742Z</published>
    <updated>2026-03-22T15:40:40.027Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 31 课：测试基础</h1><blockquote><p>学习定位：这是整套 Go 教程的第 31 课，也是阶段五（并发与工程阶段）的第七课。<br>前置要求：已经完成第 30 课，掌握了常见并发设计模式。需要熟悉函数、结构体、错误处理和包管理。<br>本课目标：掌握 Go 的 <code>testing</code> 包，理解单元测试的写法和命名约定，学会表驱动测试这一 Go 最经典的测试范式，熟悉 <code>go test</code> 命令的常用参数，能为自己写的代码编写可靠的测试。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>你已经写了 30 课的代码了。每次写完一个函数，你怎么确认它是对的？大概率是 <code>fmt.Println</code> 打印一下看看结果对不对。</p><p>这有几个问题：</p><ul><li>每次改完代码，你得手动再运行一遍看输出</li><li>多个函数的验证代码混在 <code>main</code> 里，越来越乱</li><li>别人拿到你的代码，不知道怎么验证它是否正确</li><li>三个月后你自己改了一个函数，不确定有没有影响其他地方</li></ul><p><strong>测试就是把&quot;手动验证&quot;变成&quot;自动验证&quot;。</strong> 你写一段代码来检查另一段代码的行为，运行一条命令就能知道所有功能是否正常。</p><p>Go 在这方面做得非常好：</p><ul><li><strong>测试是内置的</strong>：不需要安装任何第三方框架</li><li><strong>命令统一</strong>：一条 <code>go test</code> 就跑所有测试</li><li><strong>约定简单</strong>：文件名以 <code>_test.go</code> 结尾，函数名以 <code>Test</code> 开头</li><li><strong>表驱动测试</strong>：Go 社区的标准测试模式，清晰且可扩展</li></ul><p>这一课讲清楚以下问题：</p><ul><li>Go 的测试文件和测试函数长什么样</li><li>怎么用 <code>t.Error</code>、<code>t.Fatal</code>、<code>t.Log</code> 报告结果</li><li>什么是表驱动测试，为什么 Go 程序员都用它</li><li><code>go test</code> 有哪些常用参数</li><li>测试覆盖率怎么看</li><li>测试辅助函数怎么写</li></ul><hr><h2 id="2-最简单的测试">2. 最简单的测试</h2><h3 id="2-1-先写一个待测函数">2.1 先写一个待测函数</h3><p>假设你有一个文件 <code>math.go</code>：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> mathutil</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add 返回两个整数的和</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Add</span><span class="params">(a, b <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> a + b</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Abs 返回整数的绝对值</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Abs</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> n &lt; <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> -n</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> n</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-2-写测试文件">2.2 写测试文件</h3><p>在同一个目录下创建 <code>math_test.go</code>：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> mathutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    result := Add(<span class="number">2</span>, <span class="number">3</span>)</span><br><span class="line">    <span class="keyword">if</span> result != <span class="number">5</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Add(2, 3) = %d, 期望 5&quot;</span>, result)</span><br><span class="line">    &#125;</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">TestAbs</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    result := Abs(<span class="number">-7</span>)</span><br><span class="line">    <span class="keyword">if</span> result != <span class="number">7</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Abs(-7) = %d, 期望 7&quot;</span>, result)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-3-运行测试">2.3 运行测试</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span></span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight apache"><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="attribute">PASS</span></span><br><span class="line"><span class="attribute">ok</span>      mathutil    <span class="number">0</span>.<span class="number">003</span>s</span><br></pre></td></tr></table></figure><p>如果某个测试失败了：</p><figure class="highlight awk"><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">--- FAIL: TestAdd (<span class="number">0.00</span>s)</span><br><span class="line">    math_test.go:<span class="number">7</span>: Add(<span class="number">2</span>, <span class="number">3</span>) = <span class="number">6</span>, 期望 <span class="number">5</span></span><br><span class="line">FAIL</span><br><span class="line"><span class="keyword">exit</span> status <span class="number">1</span></span><br><span class="line">FAIL    mathutil    <span class="number">0.003</span>s</span><br></pre></td></tr></table></figure><h3 id="2-4-三条铁规">2.4 三条铁规</h3><p>Go 测试有三条约定，必须遵守：</p><table><thead><tr><th>约定</th><th>规则</th><th>示例</th></tr></thead><tbody><tr><td>文件名</td><td>以 <code>_test.go</code> 结尾</td><td><code>math_test.go</code></td></tr><tr><td>函数名</td><td>以 <code>Test</code> 开头，后面跟大写字母</td><td><code>TestAdd</code>、<code>TestAbs</code></td></tr><tr><td>参数</td><td>只有一个 <code>*testing.T</code> 参数</td><td><code>func TestAdd(t *testing.T)</code></td></tr></tbody></table><p>不符合这三条的不会被 <code>go test</code> 识别。</p><p>注意：<code>Test</code> 后面必须跟大写字母或下划线。<code>Testadd</code> 不行（小写 a），<code>TestAdd</code> 可以，<code>Test_add</code> 也可以。</p><hr><h2 id="3-报告测试结果的方法">3. 报告测试结果的方法</h2><p><code>testing.T</code> 提供了几种报告结果的方法，理解它们的区别很重要。</p><h3 id="3-1-四个关键方法">3.1 四个关键方法</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestExample</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 1. t.Log / t.Logf — 记录信息，测试继续</span></span><br><span class="line">    t.Log(<span class="string">&quot;开始测试...&quot;</span>)</span><br><span class="line">    t.Logf(<span class="string">&quot;测试参数: %d&quot;</span>, <span class="number">42</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. t.Error / t.Errorf — 报告失败，测试继续</span></span><br><span class="line">    <span class="keyword">if</span> got := Add(<span class="number">1</span>, <span class="number">2</span>); got != <span class="number">3</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Add(1,2) = %d, 期望 3&quot;</span>, got)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 这里还会继续执行</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. t.Fatal / t.Fatalf — 报告失败，测试立即停止</span></span><br><span class="line">    <span class="keyword">if</span> got := Add(<span class="number">0</span>, <span class="number">0</span>); got != <span class="number">0</span> &#123;</span><br><span class="line">        t.Fatalf(<span class="string">&quot;Add(0,0) = %d, 期望 0&quot;</span>, got)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 如果上面失败了，这里不会执行</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. t.Skip / t.Skipf — 跳过测试</span></span><br><span class="line">    <span class="keyword">if</span> testing.Short() &#123;</span><br><span class="line">        t.Skip(<span class="string">&quot;跳过耗时测试&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-什么时候用哪个">3.2 什么时候用哪个</h3><table><thead><tr><th>方法</th><th>效果</th><th>使用场景</th></tr></thead><tbody><tr><td><code>t.Error</code></td><td>标记失败，<strong>继续执行</strong></td><td>一个测试函数里检查多个条件</td></tr><tr><td><code>t.Fatal</code></td><td>标记失败，<strong>立即停止</strong></td><td>后续断言依赖当前结果（比如检查返回值前先检查 err == nil）</td></tr><tr><td><code>t.Log</code></td><td>记录信息（仅 <code>-v</code> 模式显示）</td><td>调试时记录中间值</td></tr><tr><td><code>t.Skip</code></td><td>跳过当前测试</td><td>某些环境下不适合运行</td></tr></tbody></table><h3 id="3-3-典型的-Error-vs-Fatal-选择">3.3 典型的 Error vs Fatal 选择</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestUserService</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    user, err := GetUser(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 用 Fatal：如果 err != nil，后面用 user 会 panic</span></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatalf(<span class="string">&quot;GetUser(1) 返回错误: %v&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 用 Error：即使名字不对，还可以继续检查其他字段</span></span><br><span class="line">    <span class="keyword">if</span> user.Name != <span class="string">&quot;张三&quot;</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;用户名 = %q, 期望 %q&quot;</span>, user.Name, <span class="string">&quot;张三&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> user.Age != <span class="number">25</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;年龄 = %d, 期望 %d&quot;</span>, user.Age, <span class="number">25</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="4-表驱动测试">4. 表驱动测试</h2><h3 id="4-1-为什么需要表驱动测试">4.1 为什么需要表驱动测试</h3><p>假设你要测试 <code>Abs</code> 函数。你需要覆盖这些情况：</p><ul><li>正数 → 返回自身</li><li>负数 → 返回相反数</li><li>零 → 返回零</li></ul><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="function"><span class="keyword">func</span> <span class="title">TestAbs</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> Abs(<span class="number">5</span>) != <span class="number">5</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Abs(5) = %d, 期望 5&quot;</span>, Abs(<span class="number">5</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> Abs(<span class="number">-3</span>) != <span class="number">3</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Abs(-3) = %d, 期望 3&quot;</span>, Abs(<span class="number">-3</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> Abs(<span class="number">0</span>) != <span class="number">0</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Abs(0) = %d, 期望 0&quot;</span>, Abs(<span class="number">0</span>))</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>三个 case 还行，如果有 20 个呢？全是重复代码。</p><h3 id="4-2-表驱动测试的标准写法">4.2 表驱动测试的标准写法</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestAbs</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">int</span></span><br><span class="line">        want  <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;正数&quot;</span>, <span class="number">5</span>, <span class="number">5</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;负数&quot;</span>, <span class="number">-3</span>, <span class="number">3</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;零&quot;</span>, <span class="number">0</span>, <span class="number">0</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;最小负数+1&quot;</span>, <span class="number">-2147483647</span>, <span class="number">2147483647</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;大正数&quot;</span>, <span class="number">999999</span>, <span class="number">999999</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := Abs(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Abs(%d) = %d, 期望 %d&quot;</span>, tt.input, got, tt.want)</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></pre></td></tr></table></figure><p>运行 <code>go test -v</code> 的输出：</p><figure class="highlight asciidoc"><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="section">=== RUN   TestAbs</span></span><br><span class="line"><span class="section">=== RUN   TestAbs/正数</span></span><br><span class="line"><span class="section">=== RUN   TestAbs/负数</span></span><br><span class="line"><span class="section">=== RUN   TestAbs/零</span></span><br><span class="line"><span class="section">=== RUN   TestAbs/最小负数+1</span></span><br><span class="line"><span class="section">=== RUN   TestAbs/大正数</span></span><br><span class="line"><span class="bullet">--- </span>PASS: TestAbs (0.00s)</span><br><span class="line"><span class="code">    --- PASS: TestAbs/正数 (0.00s)</span></span><br><span class="line"><span class="code">    --- PASS: TestAbs/负数 (0.00s)</span></span><br><span class="line"><span class="code">    --- PASS: TestAbs/零 (0.00s)</span></span><br><span class="line"><span class="code">    --- PASS: TestAbs/最小负数+1 (0.00s)</span></span><br><span class="line"><span class="code">    --- PASS: TestAbs/大正数 (0.00s)</span></span><br><span class="line">PASS</span><br></pre></td></tr></table></figure><h3 id="4-3-表驱动测试的结构拆解">4.3 表驱动测试的结构拆解</h3><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="comment">// 第一步：定义测试用例的结构</span></span><br><span class="line">tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">    name  <span class="type">string</span>  <span class="comment">// 用例名称（必须有，方便定位失败）</span></span><br><span class="line">    input <span class="type">int</span>     <span class="comment">// 输入</span></span><br><span class="line">    want  <span class="type">int</span>     <span class="comment">// 期望输出</span></span><br><span class="line">&#125;&#123;</span><br><span class="line">    <span class="comment">// 第二步：列出所有测试用例</span></span><br><span class="line">    &#123;<span class="string">&quot;正数&quot;</span>, <span class="number">5</span>, <span class="number">5</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;负数&quot;</span>, <span class="number">-3</span>, <span class="number">3</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第三步：遍历并用 t.Run 运行每个子测试</span></span><br><span class="line"><span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">    t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        got := Abs(tt.input)</span><br><span class="line">        <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">            t.Errorf(<span class="string">&quot;Abs(%d) = %d, 期望 %d&quot;</span>, tt.input, got, tt.want)</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><h3 id="4-4-为什么用-t-Run">4.4 为什么用 t.Run</h3><p><code>t.Run</code> 创建<strong>子测试</strong>。好处：</p><ol><li><strong>每个 case 有名字</strong>：失败时直接告诉你是哪个 case 挂了</li><li><strong>可以单独运行某个 case</strong>：<code>go test -run TestAbs/负数</code></li><li><strong>子测试之间互不影响</strong>：一个 case 失败不影响其他 case 继续运行</li><li><strong>支持并行</strong>：<code>t.Parallel()</code> 让子测试并行执行</li></ol><h3 id="4-5-更复杂的表驱动测试：带错误检查">4.5 更复杂的表驱动测试：带错误检查</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> mathutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;errors&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> ErrDivisionByZero = errors.New(<span class="string">&quot;除数不能为零&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Divide</span><span class="params">(a, b <span class="type">float64</span>)</span></span> (<span class="type">float64</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> b == <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>, ErrDivisionByZero</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> a / b, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestDivide</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name    <span class="type">string</span></span><br><span class="line">        a, b    <span class="type">float64</span></span><br><span class="line">        want    <span class="type">float64</span></span><br><span class="line">        wantErr <span class="type">error</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;正常除法&quot;</span>, <span class="number">10</span>, <span class="number">3</span>, <span class="number">3.3333333333333335</span>, <span class="literal">nil</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;整除&quot;</span>, <span class="number">10</span>, <span class="number">2</span>, <span class="number">5</span>, <span class="literal">nil</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;被除数为零&quot;</span>, <span class="number">0</span>, <span class="number">5</span>, <span class="number">0</span>, <span class="literal">nil</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;除以零&quot;</span>, <span class="number">10</span>, <span class="number">0</span>, <span class="number">0</span>, ErrDivisionByZero&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;负数除法&quot;</span>, <span class="number">-6</span>, <span class="number">3</span>, <span class="number">-2</span>, <span class="literal">nil</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got, err := Divide(tt.a, tt.b)</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 先检查错误</span></span><br><span class="line">            <span class="keyword">if</span> tt.wantErr != <span class="literal">nil</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">                    t.Fatalf(<span class="string">&quot;Divide(%g, %g) 期望错误 %v, 但没有返回错误&quot;</span>,</span><br><span class="line">                        tt.a, tt.b, tt.wantErr)</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">if</span> !errors.Is(err, tt.wantErr) &#123;</span><br><span class="line">                    t.Fatalf(<span class="string">&quot;Divide(%g, %g) 错误 = %v, 期望 %v&quot;</span>,</span><br><span class="line">                        tt.a, tt.b, err, tt.wantErr)</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">return</span>  <span class="comment">// 有错误就不检查返回值了</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 再检查正常返回值</span></span><br><span class="line">            <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">                t.Fatalf(<span class="string">&quot;Divide(%g, %g) 意外错误: %v&quot;</span>, tt.a, tt.b, err)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Divide(%g, %g) = %g, 期望 %g&quot;</span>,</span><br><span class="line">                    tt.a, tt.b, got, tt.want)</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></pre></td></tr></table></figure><h3 id="4-6-表驱动测试的核心优势">4.6 表驱动测试的核心优势</h3><table><thead><tr><th>优势</th><th>说明</th></tr></thead><tbody><tr><td>易扩展</td><td>新增 case 只需要加一行</td></tr><tr><td>结构清晰</td><td>输入、期望输出一目了然</td></tr><tr><td>减少重复</td><td>验证逻辑只写一次</td></tr><tr><td>定位方便</td><td>子测试名称直接指出哪个 case 失败</td></tr><tr><td>可维护</td><td>修改验证逻辑时只改一处</td></tr></tbody></table><hr><h2 id="5-go-test-命令详解">5. go test 命令详解</h2><h3 id="5-1-常用参数">5.1 常用参数</h3><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 运行当前包的所有测试</span></span><br><span class="line">go <span class="built_in">test</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 显示详细输出（包括通过的测试和 t.Log）</span></span><br><span class="line">go <span class="built_in">test</span> -v</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行所有子包的测试</span></span><br><span class="line">go <span class="built_in">test</span> ./...</span><br><span class="line"></span><br><span class="line"><span class="comment"># 只运行名字匹配的测试（支持正则）</span></span><br><span class="line">go <span class="built_in">test</span> -run TestAdd</span><br><span class="line">go <span class="built_in">test</span> -run TestAbs/负数</span><br><span class="line">go <span class="built_in">test</span> -run <span class="string">&quot;Test(Add|Abs)&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 显示测试覆盖率</span></span><br><span class="line">go <span class="built_in">test</span> -cover</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置超时时间（默认 10 分钟）</span></span><br><span class="line">go <span class="built_in">test</span> -<span class="built_in">timeout</span> 30s</span><br><span class="line"></span><br><span class="line"><span class="comment"># 短模式（跳过耗时测试）</span></span><br><span class="line">go <span class="built_in">test</span> -short</span><br><span class="line"></span><br><span class="line"><span class="comment"># 多次运行（检查偶发失败）</span></span><br><span class="line">go <span class="built_in">test</span> -count 5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 禁用缓存</span></span><br><span class="line">go <span class="built_in">test</span> -count=1</span><br><span class="line"></span><br><span class="line"><span class="comment"># 并行度控制</span></span><br><span class="line">go <span class="built_in">test</span> -parallel 4</span><br></pre></td></tr></table></figure><h3 id="5-2-常见用法组合">5.2 常见用法组合</h3><figure class="highlight bash"><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="comment"># 开发时最常用：详细输出 + 指定测试</span></span><br><span class="line">go <span class="built_in">test</span> -v -run TestDivide</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提交前跑一遍全部测试</span></span><br><span class="line">go <span class="built_in">test</span> ./...</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看覆盖率详情</span></span><br><span class="line">go <span class="built_in">test</span> -cover -coverprofile=coverage.out</span><br><span class="line">go tool cover -html=coverage.out    <span class="comment"># 在浏览器中查看</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 调试偶发失败</span></span><br><span class="line">go <span class="built_in">test</span> -v -count 10 -run TestFlaky</span><br><span class="line"></span><br><span class="line"><span class="comment"># CI 中常用：超时 + 全部 + 详细</span></span><br><span class="line">go <span class="built_in">test</span> -v -<span class="built_in">timeout</span> 5m ./...</span><br></pre></td></tr></table></figure><h3 id="5-3-测试缓存">5.3 测试缓存</h3><p>Go 会缓存测试结果。如果代码没变，再次运行 <code>go test</code> 会直接用缓存的结果（输出带 <code>(cached)</code> 标记）：</p><figure class="highlight erlang-repl"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ok      mathutil    (cached)</span><br></pre></td></tr></table></figure><p>想强制重新运行：</p><figure class="highlight bash"><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">go <span class="built_in">test</span> -count=1    <span class="comment"># 最常用的方式</span></span><br><span class="line">go clean -testcache  <span class="comment"># 清除所有测试缓存</span></span><br></pre></td></tr></table></figure><hr><h2 id="6-测试覆盖率">6. 测试覆盖率</h2><h3 id="6-1-什么是覆盖率">6.1 什么是覆盖率</h3><p>测试覆盖率告诉你：<strong>你的测试跑过了代码的百分之多少</strong>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -cover</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight apache"><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="attribute">PASS</span></span><br><span class="line"><span class="attribute">coverage</span>: <span class="number">85</span>.<span class="number">7</span>% of statements</span><br><span class="line"><span class="attribute">ok</span>      mathutil    <span class="number">0</span>.<span class="number">003</span>s</span><br></pre></td></tr></table></figure><p>表示你的测试执行了被测代码中 85.7% 的语句。</p><h3 id="6-2-生成覆盖率报告">6.2 生成覆盖率报告</h3><figure class="highlight bash"><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"><span class="comment"># 步骤 1：生成覆盖率数据文件</span></span><br><span class="line">go <span class="built_in">test</span> -coverprofile=coverage.out</span><br><span class="line"></span><br><span class="line"><span class="comment"># 步骤 2：在终端查看每个函数的覆盖率</span></span><br><span class="line">go tool cover -func=coverage.out</span><br><span class="line"></span><br><span class="line"><span class="comment"># 步骤 3：在浏览器中可视化查看</span></span><br><span class="line">go tool cover -html=coverage.out</span><br></pre></td></tr></table></figure><p><code>go tool cover -func</code> 的输出：</p><figure class="highlight apache"><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="attribute">mathutil</span>/math.go:<span class="number">4</span>:     Add             <span class="number">100</span>.<span class="number">0</span>%</span><br><span class="line"><span class="attribute">mathutil</span>/math.go:<span class="number">9</span>:     Abs             <span class="number">100</span>.<span class="number">0</span>%</span><br><span class="line"><span class="attribute">mathutil</span>/math.go:<span class="number">17</span>:    Divide          <span class="number">80</span>.<span class="number">0</span>%</span><br><span class="line"><span class="attribute">total</span>:                  (statements)    <span class="number">85</span>.<span class="number">7</span>%</span><br></pre></td></tr></table></figure><h3 id="6-3-覆盖率的正确态度">6.3 覆盖率的正确态度</h3><ul><li><strong>100% 覆盖率不是目标</strong>：追求 100% 往往导致写出大量无意义的测试</li><li><strong>关注关键逻辑的覆盖</strong>：错误处理分支、边界条件、核心业务逻辑</li><li><strong>覆盖率低不一定坏</strong>：某些代码（如 <code>main</code> 函数、UI 层）不适合单元测试</li><li><strong>覆盖率高不一定好</strong>：测试可能只是跑过了代码但没有正确断言</li></ul><p>实用标准：核心业务逻辑 80%+，工具函数 90%+，整体项目 60%+ 就不错。</p><hr><h2 id="7-测试辅助函数">7. 测试辅助函数</h2><h3 id="7-1-t-Helper">7.1 t.Helper()</h3><p>当你提取公共的检查逻辑时，用 <code>t.Helper()</code> 标记辅助函数：</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="comment">// 辅助函数：检查两个整数是否相等</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">assertEqual</span><span class="params">(t *testing.T, got, want <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    t.Helper()  <span class="comment">// 关键：标记为辅助函数</span></span><br><span class="line">    <span class="keyword">if</span> got != want &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;got %d, want %d&quot;</span>, got, want)</span><br><span class="line">    &#125;</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">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    assertEqual(t, Add(<span class="number">2</span>, <span class="number">3</span>), <span class="number">5</span>)   <span class="comment">// 失败时报告这一行，而不是 assertEqual 内部</span></span><br><span class="line">    assertEqual(t, Add(<span class="number">0</span>, <span class="number">0</span>), <span class="number">0</span>)</span><br><span class="line">    assertEqual(t, Add(<span class="number">-1</span>, <span class="number">1</span>), <span class="number">0</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不加 <code>t.Helper()</code>，失败时错误信息指向 <code>assertEqual</code> 函数内部——你还得猜是哪个调用挂了。加了之后，错误信息指向调用 <code>assertEqual</code> 的那一行，定位更快。</p><h3 id="7-2-t-Cleanup">7.2 t.Cleanup()</h3><p>注册清理函数，测试结束后自动执行（类似 <code>defer</code>，但专门为测试设计）：</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="function"><span class="keyword">func</span> <span class="title">TestWithTempFile</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 创建临时文件</span></span><br><span class="line">    f, err := os.CreateTemp(<span class="string">&quot;&quot;</span>, <span class="string">&quot;test-*.txt&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 注册清理：测试结束后自动删除</span></span><br><span class="line">    t.Cleanup(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        os.Remove(f.Name())</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用临时文件进行测试...</span></span><br><span class="line">    _, err = f.WriteString(<span class="string">&quot;test data&quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line">    f.Close()</span><br><span class="line"></span><br><span class="line">    data, err := os.ReadFile(f.Name())</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> <span class="type">string</span>(data) != <span class="string">&quot;test data&quot;</span> &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;文件内容 = %q, 期望 %q&quot;</span>, <span class="type">string</span>(data), <span class="string">&quot;test data&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-3-t-TempDir">7.3 t.TempDir()</h3><p>Go 1.15+ 提供了更简单的临时目录方法：</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="function"><span class="keyword">func</span> <span class="title">TestWithTempDir</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    dir := t.TempDir()  <span class="comment">// 自动创建，测试结束自动删除</span></span><br><span class="line"></span><br><span class="line">    path := filepath.Join(dir, <span class="string">&quot;test.txt&quot;</span>)</span><br><span class="line">    err := os.WriteFile(path, []<span class="type">byte</span>(<span class="string">&quot;hello&quot;</span>), <span class="number">0644</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ... 测试逻辑 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="8-子测试与并行测试">8. 子测试与并行测试</h2><h3 id="8-1-子测试的嵌套">8.1 子测试的嵌套</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestMathOperations</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    t.Run(<span class="string">&quot;加法&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        t.Run(<span class="string">&quot;正数相加&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            assertEqual(t, Add(<span class="number">2</span>, <span class="number">3</span>), <span class="number">5</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">        t.Run(<span class="string">&quot;负数相加&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            assertEqual(t, Add(<span class="number">-2</span>, <span class="number">-3</span>), <span class="number">-5</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    t.Run(<span class="string">&quot;绝对值&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        t.Run(<span class="string">&quot;正数&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            assertEqual(t, Abs(<span class="number">5</span>), <span class="number">5</span>)</span><br><span class="line">        &#125;)</span><br><span class="line">        t.Run(<span class="string">&quot;负数&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            assertEqual(t, Abs(<span class="number">-5</span>), <span class="number">5</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>运行指定层级：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go <span class="built_in">test</span> -v -run TestMathOperations/加法/正数相加</span><br></pre></td></tr></table></figure><h3 id="8-2-并行测试">8.2 并行测试</h3><p>对于互不依赖的测试，可以用 <code>t.Parallel()</code> 并行执行：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestParallel</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">int</span></span><br><span class="line">        want  <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;case1&quot;</span>, <span class="number">5</span>, <span class="number">5</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;case2&quot;</span>, <span class="number">-3</span>, <span class="number">3</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;case3&quot;</span>, <span class="number">0</span>, <span class="number">0</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            t.Parallel()  <span class="comment">// 标记为可并行</span></span><br><span class="line"></span><br><span class="line">            got := Abs(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Abs(%d) = %d, 期望 %d&quot;</span>, tt.input, got, tt.want)</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></pre></td></tr></table></figure><p><strong>注意</strong>：使用 <code>t.Parallel()</code> 时，循环变量需要在 Go 1.22 之前手动捕获。Go 1.22+ 修复了循环变量的问题，不再需要手动捕获。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Go 1.21 及之前：需要手动捕获</span></span><br><span class="line"><span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">    tt := tt  <span class="comment">// 捕获循环变量</span></span><br><span class="line">    t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        t.Parallel()</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Go 1.22+：不需要了，直接用</span></span><br><span class="line"><span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">    t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        t.Parallel()</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="9-完整实战示例">9. 完整实战示例</h2><h3 id="9-1-被测代码：字符串工具包">9.1 被测代码：字符串工具包</h3><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><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// stringutil/stringutil.go</span></span><br><span class="line"><span class="keyword">package</span> stringutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;strings&quot;</span></span><br><span class="line">    <span class="string">&quot;unicode&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Reverse 反转字符串（支持中文）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Reverse</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(runes)<span class="number">-1</span>; i &lt; j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">        runes[i], runes[j] = runes[j], runes[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// IsPalindrome 判断是否是回文（忽略大小写和空格）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IsPalindrome</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="comment">// 移除空格并转小写</span></span><br><span class="line">    s = strings.ReplaceAll(s, <span class="string">&quot; &quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">    s = strings.ToLower(s)</span><br><span class="line"></span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">for</span> i, j := <span class="number">0</span>, <span class="built_in">len</span>(runes)<span class="number">-1</span>; i &lt; j; i, j = i+<span class="number">1</span>, j<span class="number">-1</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> runes[i] != runes[j] &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// WordCount 统计字符串中的单词数</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WordCount</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">len</span>(strings.Fields(s))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Capitalize 将字符串的首字母大写</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Capitalize</span><span class="params">(s <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> s == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> s</span><br><span class="line">    &#125;</span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    runes[<span class="number">0</span>] = unicode.ToUpper(runes[<span class="number">0</span>])</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Truncate 截断字符串到指定长度，超出部分用 &quot;...&quot; 替换</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Truncate</span><span class="params">(s <span class="type">string</span>, maxLen <span class="type">int</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> maxLen &lt;= <span class="number">0</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">    runes := []<span class="type">rune</span>(s)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(runes) &lt;= maxLen &#123;</span><br><span class="line">        <span class="keyword">return</span> s</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> maxLen &lt;= <span class="number">3</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="type">string</span>(runes[:maxLen])</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">string</span>(runes[:maxLen<span class="number">-3</span>]) + <span class="string">&quot;...&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-测试代码">9.2 测试代码</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// stringutil/stringutil_test.go</span></span><br><span class="line"><span class="keyword">package</span> stringutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestReverse</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">string</span></span><br><span class="line">        want  <span class="type">string</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;英文&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="string">&quot;olleh&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;中文&quot;</span>, <span class="string">&quot;你好世界&quot;</span>, <span class="string">&quot;界世好你&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;空字符串&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;单字符&quot;</span>, <span class="string">&quot;a&quot;</span>, <span class="string">&quot;a&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;回文&quot;</span>, <span class="string">&quot;abcba&quot;</span>, <span class="string">&quot;abcba&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;带空格&quot;</span>, <span class="string">&quot;hello world&quot;</span>, <span class="string">&quot;dlrow olleh&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;中英混合&quot;</span>, <span class="string">&quot;Go语言&quot;</span>, <span class="string">&quot;言语oG&quot;</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := Reverse(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Reverse(%q) = %q, 期望 %q&quot;</span>, tt.input, got, tt.want)</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"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestIsPalindrome</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">string</span></span><br><span class="line">        want  <span class="type">bool</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;英文回文&quot;</span>, <span class="string">&quot;racecar&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;非回文&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="literal">false</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;带空格回文&quot;</span>, <span class="string">&quot;was it a car or a cat I saw&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;大小写混合&quot;</span>, <span class="string">&quot;RaceCar&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;空字符串&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;单字符&quot;</span>, <span class="string">&quot;a&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;两个字符相同&quot;</span>, <span class="string">&quot;aa&quot;</span>, <span class="literal">true</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;两个字符不同&quot;</span>, <span class="string">&quot;ab&quot;</span>, <span class="literal">false</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := IsPalindrome(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;IsPalindrome(%q) = %v, 期望 %v&quot;</span>, tt.input, got, tt.want)</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"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestWordCount</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">string</span></span><br><span class="line">        want  <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;正常句子&quot;</span>, <span class="string">&quot;hello world foo&quot;</span>, <span class="number">3</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;单个单词&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">1</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;空字符串&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="number">0</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;多余空格&quot;</span>, <span class="string">&quot;  hello   world  &quot;</span>, <span class="number">2</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;制表符分隔&quot;</span>, <span class="string">&quot;hello\tworld&quot;</span>, <span class="number">2</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;换行分隔&quot;</span>, <span class="string">&quot;hello\nworld&quot;</span>, <span class="number">2</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := WordCount(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;WordCount(%q) = %d, 期望 %d&quot;</span>, tt.input, got, tt.want)</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"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestCapitalize</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">string</span></span><br><span class="line">        want  <span class="type">string</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;小写开头&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="string">&quot;Hello&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;已经大写&quot;</span>, <span class="string">&quot;Hello&quot;</span>, <span class="string">&quot;Hello&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;空字符串&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;单字符&quot;</span>, <span class="string">&quot;a&quot;</span>, <span class="string">&quot;A&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;中文开头&quot;</span>, <span class="string">&quot;你好&quot;</span>, <span class="string">&quot;你好&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;数字开头&quot;</span>, <span class="string">&quot;123abc&quot;</span>, <span class="string">&quot;123abc&quot;</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := Capitalize(tt.input)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Capitalize(%q) = %q, 期望 %q&quot;</span>, tt.input, got, tt.want)</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"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestTruncate</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name   <span class="type">string</span></span><br><span class="line">        input  <span class="type">string</span></span><br><span class="line">        maxLen <span class="type">int</span></span><br><span class="line">        want   <span class="type">string</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;不需要截断&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">10</span>, <span class="string">&quot;hello&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;刚好等于长度&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">5</span>, <span class="string">&quot;hello&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;需要截断&quot;</span>, <span class="string">&quot;hello world&quot;</span>, <span class="number">8</span>, <span class="string">&quot;hello...&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;截断中文&quot;</span>, <span class="string">&quot;你好世界早上好&quot;</span>, <span class="number">5</span>, <span class="string">&quot;你好...&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;空字符串&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="number">5</span>, <span class="string">&quot;&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;maxLen为0&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">0</span>, <span class="string">&quot;&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;maxLen为1&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">1</span>, <span class="string">&quot;h&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;maxLen为3&quot;</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">3</span>, <span class="string">&quot;hel&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;maxLen为4&quot;</span>, <span class="string">&quot;hello world&quot;</span>, <span class="number">4</span>, <span class="string">&quot;h...&quot;</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got := Truncate(tt.input, tt.maxLen)</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Truncate(%q, %d) = %q, 期望 %q&quot;</span>,</span><br><span class="line">                    tt.input, tt.maxLen, got, tt.want)</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></pre></td></tr></table></figure><h3 id="9-3-运行效果">9.3 运行效果</h3><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">$ go <span class="built_in">test</span> -v</span><br><span class="line">=== RUN   TestReverse</span><br><span class="line">=== RUN   TestReverse/英文</span><br><span class="line">=== RUN   TestReverse/中文</span><br><span class="line">=== RUN   TestReverse/空字符串</span><br><span class="line">=== RUN   TestReverse/单字符</span><br><span class="line">=== RUN   TestReverse/回文</span><br><span class="line">=== RUN   TestReverse/带空格</span><br><span class="line">=== RUN   TestReverse/中英混合</span><br><span class="line">--- PASS: TestReverse (0.00s)</span><br><span class="line">    --- PASS: TestReverse/英文 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/中文 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/空字符串 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/单字符 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/回文 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/带空格 (0.00s)</span><br><span class="line">    --- PASS: TestReverse/中英混合 (0.00s)</span><br><span class="line">=== RUN   TestIsPalindrome</span><br><span class="line">...</span><br><span class="line">--- PASS: TestIsPalindrome (0.00s)</span><br><span class="line">=== RUN   TestWordCount</span><br><span class="line">...</span><br><span class="line">--- PASS: TestWordCount (0.00s)</span><br><span class="line">=== RUN   TestCapitalize</span><br><span class="line">...</span><br><span class="line">--- PASS: TestCapitalize (0.00s)</span><br><span class="line">=== RUN   TestTruncate</span><br><span class="line">...</span><br><span class="line">--- PASS: TestTruncate (0.00s)</span><br><span class="line">PASS</span><br><span class="line">ok      stringutil    0.004s</span><br></pre></td></tr></table></figure><hr><h2 id="10-常见坑总结">10. 常见坑总结</h2><h3 id="10-1-文件名或函数名不对">10.1 文件名或函数名不对</h3><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"><span class="comment">// 坏：文件名不以 _test.go 结尾</span></span><br><span class="line"><span class="comment">// math_tests.go   ← go test 不会识别这个文件</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 坏：函数名 Test 后面跟小写字母</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Testadd</span><span class="params">(t *testing.T)</span></span> &#123;&#125;  <span class="comment">// 不会被执行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 好</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;&#125;  <span class="comment">// Test + 大写字母</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Test_add</span><span class="params">(t *testing.T)</span></span> &#123;&#125; <span class="comment">// Test + 下划线也行</span></span><br></pre></td></tr></table></figure><h3 id="10-2-测试函数中用了-fmt-Println">10.2 测试函数中用了 fmt.Println</h3><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="comment">// 坏：用 fmt.Println 输出调试信息</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    result := Add(<span class="number">2</span>, <span class="number">3</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;result:&quot;</span>, result)  <span class="comment">// 总是打印，不受 -v 控制</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：用 t.Log</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    result := Add(<span class="number">2</span>, <span class="number">3</span>)</span><br><span class="line">    t.Log(<span class="string">&quot;result:&quot;</span>, result)  <span class="comment">// 只在 -v 模式或测试失败时打印</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-3-表驱动测试中没有给-case-命名">10.3 表驱动测试中没有给 case 命名</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：没有名字，失败时不知道是哪个 case</span></span><br><span class="line">tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">    input <span class="type">int</span></span><br><span class="line">    want  <span class="type">int</span></span><br><span class="line">&#125;&#123;</span><br><span class="line">    &#123;<span class="number">5</span>, <span class="number">5</span>&#125;,</span><br><span class="line">    &#123;<span class="number">-3</span>, <span class="number">3</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">    got := Abs(tt.input)</span><br><span class="line">    <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">        t.Errorf(<span class="string">&quot;Abs(%d) = %d, 期望 %d&quot;</span>, tt.input, got, tt.want)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：有名字 + t.Run</span></span><br><span class="line">tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">    name  <span class="type">string</span></span><br><span class="line">    input <span class="type">int</span></span><br><span class="line">    want  <span class="type">int</span></span><br><span class="line">&#125;&#123;</span><br><span class="line">    &#123;<span class="string">&quot;正数&quot;</span>, <span class="number">5</span>, <span class="number">5</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;负数&quot;</span>, <span class="number">-3</span>, <span class="number">3</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">    t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-4-该用-Fatal-的地方用了-Error">10.4 该用 Fatal 的地方用了 Error</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：err 不为 nil 时继续用 result 会 panic</span></span><br><span class="line">result, err := SomeFunction()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    t.Errorf(<span class="string">&quot;unexpected error: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 如果 err != nil，result 可能是零值，下面会出错</span></span><br><span class="line"><span class="keyword">if</span> result.Name != <span class="string">&quot;test&quot;</span> &#123;  <span class="comment">// 可能 panic</span></span><br><span class="line">    t.Error(<span class="string">&quot;wrong name&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：用 Fatal 阻止继续执行</span></span><br><span class="line">result, err := SomeFunction()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    t.Fatalf(<span class="string">&quot;unexpected error: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> result.Name != <span class="string">&quot;test&quot;</span> &#123;</span><br><span class="line">    t.Error(<span class="string">&quot;wrong name&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-5-测试之间有依赖">10.5 测试之间有依赖</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：Test2 依赖 Test1 创建的全局状态</span></span><br><span class="line"><span class="keyword">var</span> globalData <span class="type">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestStep1</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    globalData = <span class="string">&quot;initialized&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">TestStep2</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 如果 TestStep1 没跑或失败了，这里就出问题</span></span><br><span class="line">    <span class="keyword">if</span> globalData != <span class="string">&quot;initialized&quot;</span> &#123;</span><br><span class="line">        t.Fatal(<span class="string">&quot;data not ready&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：每个测试独立，自己准备自己的数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestStep2</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    data := <span class="string">&quot;initialized&quot;</span>  <span class="comment">// 自己准备数据</span></span><br><span class="line">    <span class="keyword">if</span> data != <span class="string">&quot;initialized&quot;</span> &#123;</span><br><span class="line">        t.Fatal(<span class="string">&quot;data not ready&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个测试函数应该是<strong>独立的</strong>。Go 不保证测试的执行顺序。</p><hr><h2 id="11-本课练习">11. 本课练习</h2><h3 id="练习-1：写一个计算器的测试">练习 1：写一个计算器的测试</h3><p>实现一个 <code>Calculator</code> 包，包含 <code>Add</code>、<code>Subtract</code>、<code>Multiply</code>、<code>Divide</code> 四个函数。为每个函数写表驱动测试，<code>Divide</code> 要测试除以零的错误情况。</p><hr><h3 id="练习-2：测试一个栈">练习 2：测试一个栈</h3><p>实现一个简单的整数栈（<code>Push</code>、<code>Pop</code>、<code>Peek</code>、<code>IsEmpty</code>、<code>Size</code>），为所有方法写测试。<code>Pop</code> 和 <code>Peek</code> 在栈为空时应返回错误，测试要覆盖这些边界情况。</p><hr><h3 id="练习-3：测试字符串验证器">练习 3：测试字符串验证器</h3><p>实现以下验证函数并写测试：</p><ul><li><code>IsEmail(s string) bool</code>：简单判断是否包含 <code>@</code> 和 <code>.</code></li><li><code>IsPhoneNumber(s string) bool</code>：判断是否是 11 位数字</li><li><code>IsStrongPassword(s string) bool</code>：至少 8 位，包含大写、小写和数字</li></ul><p>每个函数至少写 6 个测试用例，覆盖正常、异常和边界情况。</p><hr><h3 id="练习-4：测试覆盖率">练习 4：测试覆盖率</h3><p>对练习 1 的代码运行覆盖率分析：</p><ol><li>用 <code>go test -cover</code> 查看覆盖率百分比</li><li>用 <code>go test -coverprofile=coverage.out</code> 生成报告</li><li>用 <code>go tool cover -func=coverage.out</code> 查看每个函数的覆盖率</li><li>如果覆盖率低于 90%，补充测试用例</li></ol><hr><h3 id="练习-5：重构已有代码的测试">练习 5：重构已有代码的测试</h3><p>回到第 23 课的排序代码，为自定义排序逻辑写表驱动测试。要求：</p><ul><li>测试按年龄排序</li><li>测试按名字排序</li><li>测试空切片</li><li>测试只有一个元素的切片</li></ul><hr><h2 id="12-自测题">12. 自测题</h2><h3 id="12-1-概念题">12.1 概念题</h3><ol><li>Go 测试文件和测试函数的命名规则是什么？</li><li><code>t.Error</code> 和 <code>t.Fatal</code> 的区别是什么？什么时候用哪个？</li><li>什么是表驱动测试？它比逐个断言的写法好在哪里？</li><li><code>t.Run</code> 创建的子测试有什么好处？</li><li><code>t.Helper()</code> 的作用是什么？不加会怎样？</li><li><code>go test -cover</code> 和 <code>go test -coverprofile</code> 有什么区别？</li><li>怎么只运行某个特定的测试函数或子测试？</li><li>为什么测试之间不应该有依赖关系？</li></ol><h3 id="12-2-代码阅读题">12.2 代码阅读题</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><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="function"><span class="keyword">func</span> <span class="title">TestProcess</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        input <span class="type">int</span></span><br><span class="line">        want  <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="number">1</span>, <span class="number">2</span>&#125;,</span><br><span class="line">        &#123;<span class="number">2</span>, <span class="number">4</span>&#125;,</span><br><span class="line">        &#123;<span class="number">3</span>, <span class="number">6</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        got, err := Process(tt.input)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            t.Errorf(<span class="string">&quot;Process(%d) error: %v&quot;</span>, tt.input, err)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">            t.Errorf(<span class="string">&quot;Process(%d) = %d, want %d&quot;</span>, tt.input, got, tt.want)</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><details><summary>点击查看答案</summary><p><strong>问题 1：没有用 <code>t.Run</code> 和测试名称</strong>。当某个 case 失败时，不容易快速定位是哪个 case 的问题。应该给每个测试用例加 <code>name</code> 字段并用 <code>t.Run</code>。</p><p><strong>问题 2：err 检查用了 <code>t.Errorf</code> 而不是 <code>t.Fatalf</code></strong>。如果 <code>Process</code> 返回了错误，<code>got</code> 可能是零值，后面的 <code>got != tt.want</code> 断言会给出误导性的错误信息。应该用 <code>t.Fatalf</code> 在出错时立即停止当前子测试。</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestProcess</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    tests := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        name  <span class="type">string</span></span><br><span class="line">        input <span class="type">int</span></span><br><span class="line">        want  <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;一倍&quot;</span>, <span class="number">1</span>, <span class="number">2</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;二倍&quot;</span>, <span class="number">2</span>, <span class="number">4</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;三倍&quot;</span>, <span class="number">3</span>, <span class="number">6</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, tt := <span class="keyword">range</span> tests &#123;</span><br><span class="line">        t.Run(tt.name, <span class="function"><span class="keyword">func</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">            got, err := Process(tt.input)</span><br><span class="line">            <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">                t.Fatalf(<span class="string">&quot;Process(%d) error: %v&quot;</span>, tt.input, err)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> got != tt.want &#123;</span><br><span class="line">                t.Errorf(<span class="string">&quot;Process(%d) = %d, want %d&quot;</span>, tt.input, got, tt.want)</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></pre></td></tr></table></figure></details><hr><h2 id="13-本课总结">13. 本课总结</h2><p>这一课你学到了 Go 测试的核心内容。</p><table><thead><tr><th>知识点</th><th>要点</th></tr></thead><tbody><tr><td>测试约定</td><td>文件 <code>_test.go</code>，函数 <code>TestXxx(t *testing.T)</code></td></tr><tr><td>报告结果</td><td><code>t.Error</code>（继续）、<code>t.Fatal</code>（停止）、<code>t.Log</code>（记录）</td></tr><tr><td>表驱动测试</td><td>定义结构体切片 + <code>t.Run</code> 遍历，Go 测试的标准范式</td></tr><tr><td>go test</td><td><code>-v</code>（详细）、<code>-run</code>（过滤）、<code>-cover</code>（覆盖率）、<code>-count</code>（禁缓存）</td></tr><tr><td>覆盖率</td><td><code>-coverprofile</code> 生成报告，<code>go tool cover</code> 可视化</td></tr><tr><td>辅助工具</td><td><code>t.Helper()</code>、<code>t.Cleanup()</code>、<code>t.TempDir()</code>、<code>t.Parallel()</code></td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>表驱动测试是 Go 的标准测试范式——每个 case 有名字、有输入、有期望输出，用 <code>t.Run</code> 遍历</strong></li><li><strong><code>t.Fatal</code> 用于前置条件检查（如 err != nil），<code>t.Error</code> 用于多个独立断言</strong></li><li><strong>每个测试必须独立——不依赖其他测试的执行结果或顺序</strong></li></ol><hr><h2 id="14-下一课预告">14. 下一课预告</h2><p>你已经会写基本测试了。下一课我们来学怎么衡量代码的性能。</p><p>下一课：<strong>Benchmark 与性能测试</strong></p><p>会重点讲：</p><ul><li>基准测试的写法：<code>BenchmarkXxx(b *testing.B)</code></li><li><code>b.N</code> 是什么，Go 怎么自动调整迭代次数</li><li>怎么用 <code>go test -bench</code> 运行基准测试</li><li>怎么对比两个实现的性能差异</li><li>基准测试的常见误区</li></ul><p>学完下一课，你就能用数据说话——“这个实现比那个快多少”，而不是凭感觉猜。</p>]]></content>
    
    
    <summary type="html">Go语言系列31</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 30 课：并发模式实战</title>
    <link href="https://yjyrichard.github.io/posts/3421fbc8.html"/>
    <id>https://yjyrichard.github.io/posts/3421fbc8.html</id>
    <published>2026-03-22T15:28:11.738Z</published>
    <updated>2026-03-22T15:40:40.025Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 30 课：并发模式实战</h1><blockquote><p>学习定位：这是整套 Go 教程的第 30 课，也是阶段五（并发与工程阶段）的第六课。<br>前置要求：已经完成第 29 课，掌握了 context 与取消机制。需要熟练运用 goroutine、channel、select、WaitGroup 和 context。<br>本课目标：掌握 Go 中最常见的并发设计模式——生产者-消费者、Worker Pool、Pipeline、Fan-out/Fan-in，能根据实际需求选择合适的并发模式，写出结构清晰、可维护的并发程序。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面五课你学了 Go 并发编程的所有工具：goroutine、channel、select、锁、context。但工具只是工具，<strong>怎么把它们组合起来解决实际问题</strong>，才是关键。</p><p>就像你知道锤子、锯子、螺丝刀怎么用，但让你做一把椅子，你不知道从哪下手。并发模式就是&quot;椅子的图纸&quot;——告诉你在什么场景下，怎么组合这些工具。</p><p>这一课讲四个最常用的并发模式：</p><table><thead><tr><th>模式</th><th>解决什么问题</th><th>类比</th></tr></thead><tbody><tr><td>生产者-消费者</td><td>一方产生数据，一方处理数据</td><td>工厂流水线</td></tr><tr><td>Worker Pool</td><td>大量任务需要限制并发数</td><td>银行柜台窗口</td></tr><tr><td>Pipeline</td><td>多步处理，步骤之间串联</td><td>汽车装配线</td></tr><tr><td>Fan-out / Fan-in</td><td>一个任务拆给多人做，最后汇总</td><td>考试阅卷</td></tr></tbody></table><p>学完这一课，你就能写出结构清晰、可维护的并发程序了。</p><hr><h2 id="2-模式一：生产者-消费者">2. 模式一：生产者-消费者</h2><h3 id="2-1-解决什么问题">2.1 解决什么问题</h3><p>一方不断<strong>产生</strong>数据，另一方不断<strong>处理</strong>数据。两方速度可能不同，需要解耦。</p><p>现实例子：</p><ul><li>日志系统：应用程序不断产生日志（生产者），后台服务写入文件（消费者）</li><li>消息队列：用户下单（生产者），后台处理订单（消费者）</li><li>爬虫：URL 发现器不断找到新链接（生产者），下载器去抓取（消费者）</li></ul><h3 id="2-2-核心结构">2.2 核心结构</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">生产者 goroutine → channel → 消费者 goroutine</span><br></pre></td></tr></table></figure><p>channel 是两者之间的缓冲区。生产者只管往 channel 里塞，消费者只管从 channel 里取。</p><h3 id="2-3-完整示例：日志处理系统">2.3 完整示例：日志处理系统</h3><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><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></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;math/rand&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="keyword">type</span> LogEntry <span class="keyword">struct</span> &#123;</span><br><span class="line">    Timestamp time.Time</span><br><span class="line">    Level     <span class="type">string</span></span><br><span class="line">    Message   <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 生产者：模拟应用程序产生日志</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">logProducer</span><span class="params">(logs <span class="keyword">chan</span>&lt;- LogEntry, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">    levels := []<span class="type">string</span>&#123;<span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;WARN&quot;</span>, <span class="string">&quot;ERROR&quot;</span>&#125;</span><br><span class="line">    messages := []<span class="type">string</span>&#123;</span><br><span class="line">        <span class="string">&quot;用户登录&quot;</span>,</span><br><span class="line">        <span class="string">&quot;查询数据库&quot;</span>,</span><br><span class="line">        <span class="string">&quot;响应超时&quot;</span>,</span><br><span class="line">        <span class="string">&quot;文件未找到&quot;</span>,</span><br><span class="line">        <span class="string">&quot;缓存命中&quot;</span>,</span><br><span class="line">        <span class="string">&quot;连接断开&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">20</span>; i++ &#123;</span><br><span class="line">        entry := LogEntry&#123;</span><br><span class="line">            Timestamp: time.Now(),</span><br><span class="line">            Level:     levels[rand.Intn(<span class="built_in">len</span>(levels))],</span><br><span class="line">            Message:   messages[rand.Intn(<span class="built_in">len</span>(messages))],</span><br><span class="line">        &#125;</span><br><span class="line">        logs &lt;- entry</span><br><span class="line">        time.Sleep(time.Duration(<span class="number">50</span>+rand.Intn(<span class="number">100</span>)) * time.Millisecond)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 消费者：处理日志（写文件、发告警等）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">logConsumer</span><span class="params">(id <span class="type">int</span>, logs &lt;-<span class="keyword">chan</span> LogEntry, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">    count := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> entry := <span class="keyword">range</span> logs &#123;</span><br><span class="line">        count++</span><br><span class="line">        fmt.Printf(<span class="string">&quot;[消费者%d] %s [%s] %s\n&quot;</span>,</span><br><span class="line">            id,</span><br><span class="line">            entry.Timestamp.Format(<span class="string">&quot;15:04:05.000&quot;</span>),</span><br><span class="line">            entry.Level,</span><br><span class="line">            entry.Message,</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 模拟处理耗时</span></span><br><span class="line">        time.Sleep(time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">150</span>)) * time.Millisecond)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;[消费者%d] 退出，共处理 %d 条日志\n&quot;</span>, id, count)</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">    logs := <span class="built_in">make</span>(<span class="keyword">chan</span> LogEntry, <span class="number">10</span>)  <span class="comment">// 缓冲区 10 条</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> producerWg sync.WaitGroup</span><br><span class="line">    <span class="keyword">var</span> consumerWg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 2 个生产者</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">2</span>; i++ &#123;</span><br><span class="line">        producerWg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> logProducer(logs, &amp;producerWg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 3 个消费者</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        consumerWg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> logConsumer(i, logs, &amp;consumerWg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等所有生产者完成后关闭 channel</span></span><br><span class="line">    producerWg.Wait()</span><br><span class="line">    <span class="built_in">close</span>(logs)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等所有消费者处理完</span></span><br><span class="line">    consumerWg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;日志处理完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-4-关键设计点">2.4 关键设计点</h3><p><strong>两组 WaitGroup</strong>：生产者和消费者各一组。流程是：</p><figure class="highlight markdown"><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="bullet">1.</span> 所有生产者启动</span><br><span class="line"><span class="bullet">2.</span> 所有消费者启动</span><br><span class="line"><span class="bullet">3.</span> 等所有生产者完成 → close(channel)</span><br><span class="line"><span class="bullet">4.</span> 消费者的 range 循环自然结束</span><br><span class="line"><span class="bullet">5.</span> 等所有消费者完成</span><br></pre></td></tr></table></figure><p><strong>缓冲区大小</strong>：<code>make(chan LogEntry, 10)</code> 允许生产者先跑一段。如果消费者暂时处理不过来，生产者不会立刻被阻塞（最多缓存 10 条）。缓冲区满了，生产者自动限速。</p><h3 id="2-5-什么时候用">2.5 什么时候用</h3><ul><li>数据产生和处理的速度不同</li><li>需要解耦生产和消费逻辑</li><li>需要多个消费者并行处理</li></ul><hr><h2 id="3-模式二：Worker-Pool（工作池）">3. 模式二：Worker Pool（工作池）</h2><h3 id="3-1-解决什么问题">3.1 解决什么问题</h3><p>你有大量任务要处理，但不想每个任务启动一个 goroutine（可能有十万个任务）。你想<strong>控制并发数</strong>——比如最多同时处理 5 个任务。</p><p>现实例子：</p><ul><li>银行柜台：客户很多，但只有 5 个窗口</li><li>数据库连接池：连接数有限，不能无限创建</li><li>批量下载：同时下载太多会被限流</li></ul><h3 id="3-2-核心结构">3.2 核心结构</h3><figure class="highlight tap"><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">                    ┌── Worker<span class="number"> 1 </span>──┐</span><br><span class="line">任务 → jobs channel ├── Worker<span class="number"> 2 </span>──├→ results channel → 收集</span><br><span class="line">                    ├── Worker<span class="number"> 3 </span>──┤</span><br><span class="line">                    └── Worker N ──┘</span><br></pre></td></tr></table></figure><p>固定数量的 worker goroutine 从 jobs channel 中取任务，处理后把结果发到 results channel。</p><h3 id="3-3-完整示例：批量图片处理">3.3 完整示例：批量图片处理</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&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="keyword">type</span> Job <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID       <span class="type">int</span></span><br><span class="line">    FileName <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Result <span class="keyword">struct</span> &#123;</span><br><span class="line">    JobID    <span class="type">int</span></span><br><span class="line">    FileName <span class="type">string</span></span><br><span class="line">    Size     <span class="type">int</span></span><br><span class="line">    Duration time.Duration</span><br><span class="line">    Error    <span class="type">error</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// worker 从 jobs 中取任务，处理后发到 results</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(ctx context.Context, id <span class="type">int</span>, jobs &lt;-<span class="keyword">chan</span> Job, results <span class="keyword">chan</span>&lt;- Result, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 收到取消信号，退出\n&quot;</span>, id)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">case</span> job, ok := &lt;-jobs:</span><br><span class="line">            <span class="keyword">if</span> !ok &#123;</span><br><span class="line">                fmt.Printf(<span class="string">&quot;Worker %d: 任务队列关闭，退出\n&quot;</span>, id)</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="comment">// 处理任务</span></span><br><span class="line">            start := time.Now()</span><br><span class="line">            time.Sleep(time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">400</span>)) * time.Millisecond)</span><br><span class="line">            size := <span class="number">1024</span> + rand.Intn(<span class="number">4096</span>)</span><br><span class="line"></span><br><span class="line">            results &lt;- Result&#123;</span><br><span class="line">                JobID:    job.ID,</span><br><span class="line">                FileName: job.FileName,</span><br><span class="line">                Size:     size,</span><br><span class="line">                Duration: time.Since(start),</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"></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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">10</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> numWorkers = <span class="number">3</span></span><br><span class="line">    <span class="keyword">const</span> numJobs = <span class="number">15</span></span><br><span class="line"></span><br><span class="line">    jobs := <span class="built_in">make</span>(<span class="keyword">chan</span> Job, numJobs)</span><br><span class="line">    results := <span class="built_in">make</span>(<span class="keyword">chan</span> Result, numJobs)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 worker pool</span></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;= numWorkers; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> worker(ctx, i, jobs, results, &amp;wg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 发送任务</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= numJobs; i++ &#123;</span><br><span class="line">        jobs &lt;- Job&#123;</span><br><span class="line">            ID:       i,</span><br><span class="line">            FileName: fmt.Sprintf(<span class="string">&quot;image_%03d.jpg&quot;</span>, i),</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">close</span>(jobs)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 用一个 goroutine 等待所有 worker 完成后关闭 results</span></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(results)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集结果</span></span><br><span class="line">    totalTime := time.Now()</span><br><span class="line">    successCount := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> r := <span class="keyword">range</span> results &#123;</span><br><span class="line">        successCount++</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  完成: %s (%dKB, 耗时 %v)\n&quot;</span>,</span><br><span class="line">            r.FileName, r.Size/<span class="number">1024</span>, r.Duration)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n处理完成: %d/%d 个文件, 总耗时 %v\n&quot;</span>,</span><br><span class="line">        successCount, numJobs, time.Since(totalTime))</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker 数量: %d, 平均每个 Worker 处理 %d 个任务\n&quot;</span>,</span><br><span class="line">        numWorkers, numJobs/numWorkers)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-4-关键设计点">3.4 关键设计点</h3><p><strong>固定 Worker 数量</strong>：不管有多少任务，始终只有 <code>numWorkers</code> 个 goroutine 在工作。这是 Worker Pool 的核心——<strong>控制并发度</strong>。</p><p><strong>jobs channel 作为任务队列</strong>：所有 worker 从同一个 channel 取任务。Go 的 channel 保证每个任务只被一个 worker 拿到。</p><p><strong>results channel 收集结果</strong>：用一个单独的 goroutine 等 worker 全部完成后关闭 results，然后主 goroutine 用 <code>range</code> 收集所有结果。</p><p><strong>配合 context</strong>：支持超时取消，避免任务处理太久时整个程序卡住。</p><h3 id="3-5-通用-Worker-Pool-模板">3.5 通用 Worker Pool 模板</h3><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="function"><span class="keyword">func</span> <span class="title">RunWorkerPool</span><span class="params">(ctx context.Context, numWorkers <span class="type">int</span>, jobs &lt;-<span class="keyword">chan</span> Job)</span></span> &lt;-<span class="keyword">chan</span> Result &#123;</span><br><span class="line">    results := <span class="built_in">make</span>(<span class="keyword">chan</span> Result)</span><br><span class="line"></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">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> job := <span class="keyword">range</span> jobs &#123;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                <span class="keyword">case</span> results &lt;- process(job):</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"></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(results)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> results</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个模板可以直接复用：传入 worker 数量和 jobs channel，返回 results channel。</p><h3 id="3-6-什么时候用">3.6 什么时候用</h3><ul><li>任务量大，但需要限制并发数</li><li>每个任务互相独立，不需要特定顺序</li><li>需要控制资源使用（CPU、网络连接、数据库连接）</li></ul><hr><h2 id="4-模式三：Pipeline（流水线）">4. 模式三：Pipeline（流水线）</h2><h3 id="4-1-解决什么问题">4.1 解决什么问题</h3><p>数据需要经过多个处理步骤，每个步骤做不同的事。步骤之间用 channel 串联，每个步骤独立并发运行。</p><p>现实例子：</p><ul><li>汽车装配线：焊接 → 喷漆 → 装配 → 检测</li><li>数据处理：读取 → 解析 → 过滤 → 转换 → 存储</li><li>视频处理：解码 → 滤镜 → 编码 → 输出</li></ul><h3 id="4-2-核心结构">4.2 核心结构</h3><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">阶段1 → channel → 阶段2 → channel → 阶段3 → channel → 输出</span><br></pre></td></tr></table></figure><p>每个阶段是一个 goroutine（或一组 goroutine），从输入 channel 读数据，处理后写到输出 channel。</p><h3 id="4-3-完整示例：订单处理流水线">4.3 完整示例：订单处理流水线</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&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="keyword">type</span> Order <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID       <span class="type">int</span></span><br><span class="line">    Product  <span class="type">string</span></span><br><span class="line">    Quantity <span class="type">int</span></span><br><span class="line">    Price    <span class="type">float64</span></span><br><span class="line">    Status   <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 1：接收订单</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">receiveOrders</span><span class="params">(ctx context.Context, orderCount <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> Order &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Order)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        products := []<span class="type">string</span>&#123;<span class="string">&quot;手机&quot;</span>, <span class="string">&quot;笔记本&quot;</span>, <span class="string">&quot;耳机&quot;</span>, <span class="string">&quot;平板&quot;</span>, <span class="string">&quot;键盘&quot;</span>&#125;</span><br><span class="line">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= orderCount; i++ &#123;</span><br><span class="line">            order := Order&#123;</span><br><span class="line">                ID:       i,</span><br><span class="line">                Product:  products[rand.Intn(<span class="built_in">len</span>(products))],</span><br><span class="line">                Quantity: <span class="number">1</span> + rand.Intn(<span class="number">5</span>),</span><br><span class="line">                Price:    <span class="type">float64</span>(<span class="number">50</span>+rand.Intn(<span class="number">950</span>)) + <span class="number">0.9</span>,</span><br><span class="line">                Status:   <span class="string">&quot;已接收&quot;</span>,</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> out &lt;- order:</span><br><span class="line">                fmt.Printf(<span class="string">&quot;[接收] 订单 #%d: %s x%d\n&quot;</span>, order.ID, order.Product, order.Quantity)</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            time.Sleep(<span class="number">100</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 2：验证订单</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">validateOrders</span><span class="params">(ctx context.Context, orders &lt;-<span class="keyword">chan</span> Order)</span></span> &lt;-<span class="keyword">chan</span> Order &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Order)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> order := <span class="keyword">range</span> orders &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            time.Sleep(<span class="number">50</span> * time.Millisecond)</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 模拟验证：数量超过 3 的拒绝</span></span><br><span class="line">            <span class="keyword">if</span> order.Quantity &gt; <span class="number">3</span> &#123;</span><br><span class="line">                fmt.Printf(<span class="string">&quot;[验证] 订单 #%d: 拒绝（数量过多: %d）\n&quot;</span>, order.ID, order.Quantity)</span><br><span class="line">                <span class="keyword">continue</span>  <span class="comment">// 不传递给下一个阶段</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            order.Status = <span class="string">&quot;已验证&quot;</span></span><br><span class="line">            fmt.Printf(<span class="string">&quot;[验证] 订单 #%d: 通过\n&quot;</span>, order.ID)</span><br><span class="line">            out &lt;- order</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 3：计算价格</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">calculatePrice</span><span class="params">(ctx context.Context, orders &lt;-<span class="keyword">chan</span> Order)</span></span> &lt;-<span class="keyword">chan</span> Order &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Order)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> order := <span class="keyword">range</span> orders &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            time.Sleep(<span class="number">80</span> * time.Millisecond)</span><br><span class="line"></span><br><span class="line">            order.Price = order.Price * <span class="type">float64</span>(order.Quantity)</span><br><span class="line">            <span class="comment">// 满 500 打九折</span></span><br><span class="line">            <span class="keyword">if</span> order.Price &gt; <span class="number">500</span> &#123;</span><br><span class="line">                order.Price *= <span class="number">0.9</span></span><br><span class="line">            &#125;</span><br><span class="line">            order.Status = <span class="string">&quot;已定价&quot;</span></span><br><span class="line">            fmt.Printf(<span class="string">&quot;[定价] 订单 #%d: ¥%.2f\n&quot;</span>, order.ID, order.Price)</span><br><span class="line">            out &lt;- order</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 4：完成订单</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">completeOrders</span><span class="params">(ctx context.Context, orders &lt;-<span class="keyword">chan</span> Order)</span></span> &lt;-<span class="keyword">chan</span> Order &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Order)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> order := <span class="keyword">range</span> orders &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            time.Sleep(<span class="number">60</span> * time.Millisecond)</span><br><span class="line"></span><br><span class="line">            order.Status = <span class="string">&quot;已完成&quot;</span></span><br><span class="line">            fmt.Printf(<span class="string">&quot;[完成] 订单 #%d: %s x%d = ¥%.2f\n&quot;</span>,</span><br><span class="line">                order.ID, order.Product, order.Quantity, order.Price)</span><br><span class="line">            out &lt;- order</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">10</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 组装流水线</span></span><br><span class="line">    orders := receiveOrders(ctx, <span class="number">8</span>)</span><br><span class="line">    validated := validateOrders(ctx, orders)</span><br><span class="line">    priced := calculatePrice(ctx, validated)</span><br><span class="line">    completed := completeOrders(ctx, priced)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 消费最终结果</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n========== 订单处理流水线 ==========\n&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> totalRevenue <span class="type">float64</span></span><br><span class="line">    count := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> order := <span class="keyword">range</span> completed &#123;</span><br><span class="line">        totalRevenue += order.Price</span><br><span class="line">        count++</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n========== 汇总 ==========\n&quot;</span>)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;完成订单: %d 个\n&quot;</span>, count)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;总收入: ¥%.2f\n&quot;</span>, totalRevenue)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-4-关键设计点">4.4 关键设计点</h3><p><strong>每个阶段返回 <code>&lt;-chan Order</code></strong>：这是 Pipeline 的标准写法。每个函数创建 channel、启动 goroutine、返回只读 channel。调用方像搭积木一样串起来：</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">orders := receiveOrders(ctx, <span class="number">8</span>)</span><br><span class="line">validated := validateOrders(ctx, orders)</span><br><span class="line">priced := calculatePrice(ctx, validated)</span><br><span class="line">completed := completeOrders(ctx, priced)</span><br></pre></td></tr></table></figure><p><strong>阶段可以过滤数据</strong>：<code>validateOrders</code> 拒绝了不合格的订单（<code>continue</code> 跳过，不传递给下一阶段）。流水线中每个阶段的输出不一定等于输入。</p><p><strong>每个阶段关闭自己的输出 channel</strong>：当输入 channel 关闭且处理完最后一个元素后，<code>defer close(out)</code> 关闭输出。信号像多米诺骨牌一样传递：第一个阶段关闭 → 第二个收到信号处理完剩余后关闭 → 依次传递。</p><h3 id="4-5-什么时候用">4.5 什么时候用</h3><ul><li>数据需要经过多个有序的处理步骤</li><li>各步骤的处理逻辑可以独立</li><li>需要步骤之间的流控（channel 缓冲）</li></ul><hr><h2 id="5-模式四：Fan-out-Fan-in（扇出-扇入）">5. 模式四：Fan-out / Fan-in（扇出 / 扇入）</h2><h3 id="5-1-解决什么问题">5.1 解决什么问题</h3><p><strong>Fan-out（扇出）</strong>：一个任务拆成多份，分配给多个 goroutine 并行处理。<br><strong>Fan-in（扇入）</strong>：多个 goroutine 的结果合并成一个 channel。</p><p>两者通常一起使用。</p><p>现实例子：</p><ul><li>考试阅卷：一叠卷子分给多个老师批（扇出），批完后汇总分数（扇入）</li><li>搜索引擎：一个查询发给多个索引分片（扇出），结果合并排序（扇入）</li><li>文件处理：把大文件分块，多个 goroutine 各自处理一块，最后合并</li></ul><h3 id="5-2-核心结构">5.2 核心结构</h3><figure class="highlight sas"><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">                    ┌── goroutine 1 ──┐</span><br><span class="line">一个 <span class="keyword">input</span> channel ─├── goroutine 2 ──├─ <span class="keyword">merge</span> ─→ 一个 <span class="keyword">output</span> channel</span><br><span class="line">                    └── goroutine 3 ──┘</span><br><span class="line">     (Fan-<span class="keyword">out</span>)                           (Fan-<span class="keyword">in</span>)</span><br></pre></td></tr></table></figure><h3 id="5-3-Fan-in-工具函数">5.3 Fan-in 工具函数</h3><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="function"><span class="keyword">func</span> <span class="title">fanIn</span><span class="params">(ctx context.Context, channels ...&lt;-<span class="keyword">chan</span> Result)</span></span> &lt;-<span class="keyword">chan</span> Result &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Result)</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, ch := <span class="keyword">range</span> channels &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(c &lt;-<span class="keyword">chan</span> Result)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> v := <span class="keyword">range</span> c &#123;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> out &lt;- v:</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;(ch)</span><br><span class="line">    &#125;</span><br><span class="line"></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个函数把多个 channel 合并成一个。每个输入 channel 对应一个 goroutine，所有数据汇聚到 <code>out</code>。</p><h3 id="5-4-完整示例：并行文件搜索">5.4 完整示例：并行文件搜索</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&quot;</span></span><br><span class="line">    <span class="string">&quot;strings&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="keyword">type</span> SearchResult <span class="keyword">struct</span> &#123;</span><br><span class="line">    WorkerID <span class="type">int</span></span><br><span class="line">    FileName <span class="type">string</span></span><br><span class="line">    Line     <span class="type">int</span></span><br><span class="line">    Content  <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 模拟搜索一批文件</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">searchWorker</span><span class="params">(ctx context.Context, id <span class="type">int</span>, files []<span class="type">string</span>, keyword <span class="type">string</span>)</span></span> &lt;-<span class="keyword">chan</span> SearchResult &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> SearchResult)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> _, file := <span class="keyword">range</span> files &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 模拟文件搜索</span></span><br><span class="line">            time.Sleep(time.Duration(<span class="number">50</span>+rand.Intn(<span class="number">150</span>)) * time.Millisecond)</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 模拟随机找到匹配</span></span><br><span class="line">            <span class="keyword">if</span> rand.Intn(<span class="number">3</span>) == <span class="number">0</span> &#123;</span><br><span class="line">                result := SearchResult&#123;</span><br><span class="line">                    WorkerID: id,</span><br><span class="line">                    FileName: file,</span><br><span class="line">                    Line:     rand.Intn(<span class="number">100</span>) + <span class="number">1</span>,</span><br><span class="line">                    Content:  fmt.Sprintf(<span class="string">&quot;... %s ...&quot;</span>, keyword),</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> out &lt;- result:</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></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">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Fan-in：合并多个 channel</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">merge</span><span class="params">(ctx context.Context, channels ...&lt;-<span class="keyword">chan</span> SearchResult)</span></span> &lt;-<span class="keyword">chan</span> SearchResult &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> SearchResult)</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, ch := <span class="keyword">range</span> channels &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(c &lt;-<span class="keyword">chan</span> SearchResult)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> v := <span class="keyword">range</span> c &#123;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> out &lt;- v:</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;(ch)</span><br><span class="line">    &#125;</span><br><span class="line"></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> out</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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">5</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟 30 个文件</span></span><br><span class="line">    allFiles := <span class="built_in">make</span>([]<span class="type">string</span>, <span class="number">30</span>)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> allFiles &#123;</span><br><span class="line">        dirs := []<span class="type">string</span>&#123;<span class="string">&quot;src&quot;</span>, <span class="string">&quot;pkg&quot;</span>, <span class="string">&quot;internal&quot;</span>, <span class="string">&quot;cmd&quot;</span>&#125;</span><br><span class="line">        exts := []<span class="type">string</span>&#123;<span class="string">&quot;.go&quot;</span>, <span class="string">&quot;.txt&quot;</span>, <span class="string">&quot;.md&quot;</span>, <span class="string">&quot;.json&quot;</span>&#125;</span><br><span class="line">        allFiles[i] = fmt.Sprintf(<span class="string">&quot;%s/file_%02d%s&quot;</span>,</span><br><span class="line">            dirs[rand.Intn(<span class="built_in">len</span>(dirs))], i, exts[rand.Intn(<span class="built_in">len</span>(exts))])</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    keyword := <span class="string">&quot;TODO&quot;</span></span><br><span class="line">    numWorkers := <span class="number">3</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Fan-out：把文件分配给多个 worker</span></span><br><span class="line">    chunkSize := (<span class="built_in">len</span>(allFiles) + numWorkers - <span class="number">1</span>) / numWorkers</span><br><span class="line">    <span class="keyword">var</span> workerChannels []&lt;-<span class="keyword">chan</span> SearchResult</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        start := i * chunkSize</span><br><span class="line">        end := start + chunkSize</span><br><span class="line">        <span class="keyword">if</span> end &gt; <span class="built_in">len</span>(allFiles) &#123;</span><br><span class="line">            end = <span class="built_in">len</span>(allFiles)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        chunk := allFiles[start:end]</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Worker %d: 负责 %d 个文件 (%s ~ %s)\n&quot;</span>,</span><br><span class="line">            i+<span class="number">1</span>, <span class="built_in">len</span>(chunk), chunk[<span class="number">0</span>], chunk[<span class="built_in">len</span>(chunk)<span class="number">-1</span>])</span><br><span class="line"></span><br><span class="line">        ch := searchWorker(ctx, i+<span class="number">1</span>, chunk, keyword)</span><br><span class="line">        workerChannels = <span class="built_in">append</span>(workerChannels, ch)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Fan-in：合并所有 worker 的结果</span></span><br><span class="line">    results := merge(ctx, workerChannels...)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集结果</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n搜索 \&quot;%s\&quot;...\n\n&quot;</span>, keyword)</span><br><span class="line">    count := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> r := <span class="keyword">range</span> results &#123;</span><br><span class="line">        count++</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  [Worker%d] %s:%d  %s\n&quot;</span>,</span><br><span class="line">            r.WorkerID, r.FileName, r.Line, r.Content)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> count == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;  没有找到匹配&quot;</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;\n共找到 %d 处匹配\n&quot;</span>, count)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    _ = strings.Contains <span class="comment">// 避免 import 警告</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-5-关键设计点">5.5 关键设计点</h3><p><strong>Fan-out</strong>：把 30 个文件平均分成 3 份，每个 worker 处理 10 个。任务分配在主 goroutine 中完成。</p><p><strong>Fan-in</strong>：<code>merge</code> 函数把 3 个 worker 的输出 channel 合并成一个。主 goroutine 只需要从合并后的 channel 中读取，不用关心数据来自哪个 worker。</p><p><strong>结果按完成顺序返回</strong>：先处理完的先输出，不需要等所有 worker 都完成。</p><h3 id="5-6-什么时候用">5.6 什么时候用</h3><ul><li>一个大任务可以拆成多个独立的小任务</li><li>需要并行加速处理</li><li>结果需要汇总到一处</li></ul><hr><h2 id="6-模式对比与选择">6. 模式对比与选择</h2><h3 id="6-1-四种模式的对比">6.1 四种模式的对比</h3><table><thead><tr><th>模式</th><th>数据流向</th><th>goroutine 数量</th><th>适用场景</th></tr></thead><tbody><tr><td>生产者-消费者</td><td>单向：生产 → 消费</td><td>固定</td><td>解耦生产和消费</td></tr><tr><td>Worker Pool</td><td>任务 → 多 Worker → 结果</td><td>固定</td><td>限制并发数</td></tr><tr><td>Pipeline</td><td>线性：A → B → C → D</td><td>每阶段一个</td><td>多步骤顺序处理</td></tr><tr><td>Fan-out/Fan-in</td><td>分散 → 汇聚</td><td>动态</td><td>并行加速</td></tr></tbody></table><h3 id="6-2-怎么选">6.2 怎么选</h3><figure class="highlight ada"><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><br><span class="line">  → Worker Pool</span><br><span class="line"></span><br><span class="line">需求：数据经过多个处理步骤？</span><br><span class="line">  → Pipeline</span><br><span class="line"></span><br><span class="line">需求：一个大任务拆成多个并行子任务？</span><br><span class="line">  → Fan-<span class="keyword">out</span> / Fan-<span class="keyword">in</span></span><br><span class="line"></span><br><span class="line">需求：一方产生数据，一方消费？</span><br><span class="line">  → 生产者-消费者</span><br></pre></td></tr></table></figure><p>实际项目中经常组合使用。比如 Pipeline 的某个阶段内部用 Worker Pool 来并行处理。</p><hr><h2 id="7-综合示例：数据处理管道">7. 综合示例：数据处理管道</h2><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&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="keyword">type</span> Record <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID     <span class="type">int</span></span><br><span class="line">    Value  <span class="type">int</span></span><br><span class="line">    Valid  <span class="type">bool</span></span><br><span class="line">    Result <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 1：生成数据（生产者）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">generate</span><span class="params">(ctx context.Context, count <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> Record &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Record)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= count; i++ &#123;</span><br><span class="line">            record := Record&#123;ID: i, Value: rand.Intn(<span class="number">100</span>)&#125;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> out &lt;- record:</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 2：验证（Pipeline 阶段，内部用 Worker Pool 加速）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">validate</span><span class="params">(ctx context.Context, in &lt;-<span class="keyword">chan</span> Record, numWorkers <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> Record &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Record)</span><br><span class="line"></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">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> record := <span class="keyword">range</span> in &#123;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                <span class="keyword">default</span>:</span><br><span class="line">                &#125;</span><br><span class="line">                time.Sleep(<span class="number">20</span> * time.Millisecond)  <span class="comment">// 模拟验证</span></span><br><span class="line">                record.Valid = record.Value &gt;= <span class="number">10</span>    <span class="comment">// 值 &gt;= 10 才有效</span></span><br><span class="line">                out &lt;- record</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 3：过滤（只保留有效记录）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">filter</span><span class="params">(ctx context.Context, in &lt;-<span class="keyword">chan</span> Record)</span></span> &lt;-<span class="keyword">chan</span> Record &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Record)</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">        <span class="keyword">defer</span> <span class="built_in">close</span>(out)</span><br><span class="line">        <span class="keyword">for</span> record := <span class="keyword">range</span> in &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> record.Valid &#123;</span><br><span class="line">                out &lt;- record</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 阶段 4：计算（Worker Pool）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">compute</span><span class="params">(ctx context.Context, in &lt;-<span class="keyword">chan</span> Record, numWorkers <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> Record &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> Record)</span><br><span class="line"></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">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> record := <span class="keyword">range</span> in &#123;</span><br><span class="line">                <span class="keyword">select</span> &#123;</span><br><span class="line">                <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                    <span class="keyword">return</span></span><br><span class="line">                <span class="keyword">default</span>:</span><br><span class="line">                &#125;</span><br><span class="line">                time.Sleep(<span class="number">30</span> * time.Millisecond)</span><br><span class="line">                record.Result = record.Value * record.Value</span><br><span class="line">                out &lt;- record</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> out</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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">10</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 组装管道：生成 → 验证(3 workers) → 过滤 → 计算(2 workers)</span></span><br><span class="line">    records := generate(ctx, <span class="number">50</span>)</span><br><span class="line">    validated := validate(ctx, records, <span class="number">3</span>)</span><br><span class="line">    filtered := filter(ctx, validated)</span><br><span class="line">    computed := compute(ctx, filtered, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集最终结果</span></span><br><span class="line">    total := <span class="number">0</span></span><br><span class="line">    count := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> record := <span class="keyword">range</span> computed &#123;</span><br><span class="line">        total += record.Result</span><br><span class="line">        count++</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;处理完成: %d 条有效记录，总和 = %d\n&quot;</span>, count, total)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;总耗时: %v\n&quot;</span>, time.Since(start))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子展示了 Pipeline + Worker Pool 的组合：</p><ul><li>整体是 Pipeline（四个阶段串联）</li><li>验证阶段和计算阶段内部各自用了 Worker Pool（多个 goroutine 并行处理）</li><li>全程配合 context，支持超时取消</li></ul><hr><h2 id="8-常见坑总结">8. 常见坑总结</h2><h3 id="8-1-goroutine-泄漏">8.1 goroutine 泄漏</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：如果 ctx 取消了，往 out 发送会阻塞（没人收了）</span></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">    <span class="keyword">for</span> record := <span class="keyword">range</span> in &#123;</span><br><span class="line">        out &lt;- process(record)  <span class="comment">// 如果消费者已经退出，这里永远阻塞</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：每次发送都检查 ctx</span></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">    <span class="keyword">for</span> record := <span class="keyword">range</span> in &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> out &lt;- process(record):</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            <span class="keyword">return</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><strong>Pipeline 的每一个 goroutine 中，发送和接收都要配合 <code>ctx.Done()</code></strong>。不然一旦下游取消，上游就永远阻塞。</p><h3 id="8-2-忘记关闭-channel">8.2 忘记关闭 channel</h3><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="comment">// 坏：Worker Pool 中忘了关闭输出 channel</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; 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">        <span class="keyword">for</span> job := <span class="keyword">range</span> jobs &#123;</span><br><span class="line">            results &lt;- process(job)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// results 永远不会关闭，下游的 range 永远不结束</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：用 WaitGroup + 单独的 goroutine 关闭</span></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">    wg.Wait()</span><br><span class="line">    <span class="built_in">close</span>(results)</span><br><span class="line">&#125;()</span><br></pre></td></tr></table></figure><h3 id="8-3-Worker-Pool-中-Worker-数量选择">8.3 Worker Pool 中 Worker 数量选择</h3><figure class="highlight x86asm"><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="meta">CPU</span> 密集型任务：Worker 数 ≈ <span class="meta">CPU</span> 核心数</span><br><span class="line">IO 密集型任务：Worker 数可以多一些（核心数 × <span class="number">2</span>~<span class="number">10</span>）</span><br><span class="line">不确定：从 runtime<span class="number">.</span>NumCPU() 开始，根据实测调整</span><br></pre></td></tr></table></figure><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">import</span> <span class="string">&quot;runtime&quot;</span></span><br><span class="line"></span><br><span class="line">numWorkers := runtime.NumCPU()  <span class="comment">// CPU 核心数</span></span><br></pre></td></tr></table></figure><h3 id="8-4-Pipeline-阶段顺序有依赖">8.4 Pipeline 阶段顺序有依赖</h3><p>Pipeline 的每个阶段必须按顺序串联。如果阶段 B 依赖阶段 A 的输出，不能把它们并行执行。但阶段内部可以用 Worker Pool 来并行。</p><hr><h2 id="9-本课练习">9. 本课练习</h2><h3 id="练习-1：文件下载器">练习 1：文件下载器</h3><p>要求：</p><ul><li>给定 20 个 URL（模拟）</li><li>用 Worker Pool 模式，最多同时下载 5 个</li><li>每个下载任务模拟随机耗时 200ms~1000ms</li><li>收集并打印每个 URL 的下载结果（成功/失败、耗时、大小）</li></ul><hr><h3 id="练习-2：日志分析流水线">练习 2：日志分析流水线</h3><p>要求：</p><ul><li>阶段 1：读取日志行（模拟 100 条）</li><li>阶段 2：解析日志（提取时间、级别、消息）</li><li>阶段 3：过滤（只保留 ERROR 和 WARN）</li><li>阶段 4：统计（按级别计数）</li><li>用 Pipeline 模式串联</li></ul><hr><h3 id="练习-3：并行单词计数">练习 3：并行单词计数</h3><p>要求：</p><ul><li>给定 10 段文本（模拟 10 个文件）</li><li>用 Fan-out 把每段文本交给一个 goroutine 统计单词频率</li><li>用 Fan-in 合并所有结果</li><li>输出总的单词频率 Top 10</li></ul><hr><h3 id="练习-4：带限速的-API-调用">练习 4：带限速的 API 调用</h3><p>要求：</p><ul><li>模拟调用一个 API 100 次</li><li>限速：每秒最多 10 次请求</li><li>用 Worker Pool + time.Ticker 实现</li><li>总超时 15 秒</li></ul><hr><h3 id="练习-5：组合模式">练习 5：组合模式</h3><p>要求：</p><ul><li>设计一个&quot;电商订单处理系统&quot;</li><li>阶段 1：接收订单（生产者）</li><li>阶段 2：验证库存（Worker Pool，3 个 worker）</li><li>阶段 3：计算价格（Pipeline）</li><li>阶段 4：发送通知（Fan-out 给邮件和短信两个 channel，Fan-in 合并确认）</li></ul><hr><h2 id="10-自测题">10. 自测题</h2><h3 id="10-1-概念题">10.1 概念题</h3><ol><li>生产者-消费者模式中，channel 缓冲区的作用是什么？</li><li>Worker Pool 模式的核心目的是什么？Worker 数量怎么选？</li><li>Pipeline 模式中，每个阶段为什么要返回 <code>&lt;-chan T</code> 而不是 <code>chan T</code>？</li><li>Fan-in 是怎么实现的？为什么需要一个单独的 goroutine 来关闭输出 channel？</li><li>为什么 Pipeline 的每个 goroutine 里发送和接收都要配合 <code>ctx.Done()</code>？</li><li>Worker Pool 和 Fan-out 有什么区别？</li><li>怎么避免 Pipeline 中的 goroutine 泄漏？</li><li>四种模式分别适合什么场景？</li></ol><h3 id="10-2-代码阅读题">10.2 代码阅读题</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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">process</span><span class="params">(in &lt;-<span class="keyword">chan</span> <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">int</span> &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</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">        <span class="keyword">for</span> v := <span class="keyword">range</span> in &#123;</span><br><span class="line">            out &lt;- v * <span class="number">2</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</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">        <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">            ch &lt;- i</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(ch)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    result := process(ch)</span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> result &#123;</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>问题：<strong><code>out</code> channel 永远不会被关闭</strong>。</p><p><code>process</code> 函数中的 goroutine 在 <code>for range in</code> 结束后退出了，但没有 <code>close(out)</code>。主 goroutine 中的 <code>for v := range result</code> 会在收到 0、2、4、6、8 后永远阻塞等待，最终报 deadlock。</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></pre></td><td class="code"><pre><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">    <span class="keyword">defer</span> <span class="built_in">close</span>(out)  <span class="comment">// 加上这一行</span></span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> in &#123;</span><br><span class="line">        out &lt;- v * <span class="number">2</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;()</span><br></pre></td></tr></table></figure><p>这是 Pipeline 模式中最常见的 bug——忘记关闭输出 channel。</p></details><hr><h2 id="11-本课总结">11. 本课总结</h2><p>这一课你学到了 Go 中最常用的四种并发设计模式。</p><table><thead><tr><th>模式</th><th>核心结构</th><th>关键 channel</th></tr></thead><tbody><tr><td>生产者-消费者</td><td>生产 → buffer → 消费</td><td>一个数据 channel</td></tr><tr><td>Worker Pool</td><td>任务 → N 个 Worker → 结果</td><td>jobs + results</td></tr><tr><td>Pipeline</td><td>A → B → C → D</td><td>每个阶段之间一个 channel</td></tr><tr><td>Fan-out/Fan-in</td><td>拆分 → 并行 → 合并</td><td>多个输入 channel → merge → 一个输出</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>Worker Pool 控制并发度——不是 goroutine 越多越好，资源有限就要限制</strong></li><li><strong>Pipeline 每个阶段返回只读 channel 并 <code>defer close(out)</code>——这是不泄漏的前提</strong></li><li><strong>所有 goroutine 的发送和接收都配合 <code>ctx.Done()</code>——确保取消时整个管道能干净退出</strong></li></ol><hr><h2 id="12-下一课预告">12. 下一课预告</h2><p>到这里你已经掌握了 Go 并发编程的核心内容。下面进入工程实践部分。</p><p>下一课：<strong>测试基础</strong></p><p>会重点讲：</p><ul><li>Go 的 <code>testing</code> 包怎么用</li><li>单元测试的写法和命名约定</li><li>表驱动测试——Go 测试的经典范式</li><li><code>go test</code> 命令的常用参数</li><li>测试覆盖率</li></ul><p>学完下一课，你就能为自己写的代码加上测试，提高代码质量了。</p>]]></content>
    
    
    <summary type="html">Go语言系列30</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 29 课：context 与取消机制</title>
    <link href="https://yjyrichard.github.io/posts/e3e36542.html"/>
    <id>https://yjyrichard.github.io/posts/e3e36542.html</id>
    <published>2026-03-22T15:28:11.733Z</published>
    <updated>2026-03-22T15:40:40.020Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 29 课：<code>context</code> 与取消机制</h1><blockquote><p>学习定位：这是整套 Go 教程的第 29 课，也是阶段五（并发与工程阶段）的第五课。<br>前置要求：已经完成第 28 课，掌握了 Mutex、RWMutex、Once 等同步原语。需要熟悉 goroutine、channel 和 select。<br>本课目标：理解 <code>context</code> 的设计目的，掌握 <code>WithCancel</code>、<code>WithTimeout</code>、<code>WithDeadline</code> 的使用方式，能在 goroutine 中监听取消信号，理解 context 的传递约定和使用规范。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>到目前为止你学了创建 goroutine、用 channel 通信、用锁保护数据。但有一个重要问题还没解决：</p><p><strong>怎么让一个正在运行的 goroutine 停下来？</strong></p><p>想象几个真实场景：</p><ul><li>用户发起了一个搜索请求，但等了 3 秒没结果就关闭了页面。后台的搜索 goroutine 还在傻傻跑着，浪费资源。</li><li>你同时向 3 个服务器发请求，第一个返回了结果。另外两个还在跑，但你已经不需要它们了。</li><li>一个任务有子任务，子任务又有子子任务。最顶层取消了，下面所有的都应该停下来。</li></ul><p>Go 1.7 引入的 <code>context</code> 包就是解决这个问题的：<strong>控制 goroutine 的生命周期</strong>。</p><p>你需要搞明白以下问题：</p><ul><li><code>context</code> 是什么，为什么需要它</li><li><code>context.Background()</code> 和 <code>context.TODO()</code> 是什么</li><li>怎么用 <code>WithCancel</code> 手动取消</li><li>怎么用 <code>WithTimeout</code> 和 <code>WithDeadline</code> 自动超时</li><li>怎么在 goroutine 中监听取消信号</li><li><code>context</code> 怎么传递，有什么规范</li><li>怎么用 <code>context</code> 传值</li></ul><hr><h2 id="2-context-是什么">2. <code>context</code> 是什么</h2><h3 id="2-1-一句话定义">2.1 一句话定义</h3><p><strong><code>context</code> 是 goroutine 的生命周期管理器。它能传递取消信号、超时限制和请求级数据。</strong></p><h3 id="2-2-核心接口">2.2 核心接口</h3><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="keyword">type</span> Context <span class="keyword">interface</span> &#123;</span><br><span class="line">    Deadline() (deadline time.Time, ok <span class="type">bool</span>)  <span class="comment">// 返回截止时间</span></span><br><span class="line">    Done() &lt;-<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;                     <span class="comment">// 返回一个 channel，取消时关闭</span></span><br><span class="line">    Err() <span class="type">error</span>                               <span class="comment">// 取消的原因</span></span><br><span class="line">    Value(key any) any                        <span class="comment">// 获取关联的值</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你不需要自己实现这个接口。标准库提供了创建 context 的函数，直接用就行。</p><p>最核心的是 <code>Done()</code> 方法——它返回一个 channel。当 context 被取消时，这个 channel 会被关闭。goroutine 通过 <code>select</code> 监听这个 channel 来知道&quot;该停下来了&quot;。</p><h3 id="2-3-context-的树形结构">2.3 context 的树形结构</h3><p>context 是<strong>父子关系</strong>：从一个父 context 派生出子 context，子 context 又能派生出孙 context。</p><figure class="highlight scss"><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="attribute">Background</span> (根)</span><br><span class="line">├── WithCancel → ctx1</span><br><span class="line">│   ├── WithTimeout → ctx2</span><br><span class="line">│   └── WithCancel → ctx3</span><br><span class="line">└── WithTimeout → ctx4</span><br></pre></td></tr></table></figure><p><strong>取消会向下传播</strong>：父 context 取消时，所有子 context 自动取消。但子 context 取消不会影响父 context。</p><hr><h2 id="3-创建-context">3. 创建 context</h2><h3 id="3-1-根-context">3.1 根 context</h3><p>每个 context 树都需要一个根。Go 提供了两个根 context：</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">ctx := context.Background()  <span class="comment">// 最常用的根 context</span></span><br><span class="line">ctx := context.TODO()        <span class="comment">// 占位用，表示&quot;还没想好用什么 context&quot;</span></span><br></pre></td></tr></table></figure><ul><li><code>Background()</code>：程序启动、main 函数、测试的顶层 context</li><li><code>TODO()</code>：暂时不确定用什么 context 时的占位符</li></ul><p>两者功能完全相同（都是空 context，永远不会取消），只是语义不同。<strong>99% 的情况用 <code>Background()</code></strong>。</p><h3 id="3-2-派生-context">3.2 派生 context</h3><p>从根 context 派生出有具体能力的子 context：</p><table><thead><tr><th>函数</th><th>作用</th></tr></thead><tbody><tr><td><code>context.WithCancel(parent)</code></td><td>手动取消</td></tr><tr><td><code>context.WithTimeout(parent, duration)</code></td><td>超时自动取消</td></tr><tr><td><code>context.WithDeadline(parent, time)</code></td><td>到达指定时间自动取消</td></tr><tr><td><code>context.WithValue(parent, key, value)</code></td><td>携带值</td></tr></tbody></table><hr><h2 id="4-WithCancel——手动取消">4. <code>WithCancel</code>——手动取消</h2><h3 id="4-1-基本用法">4.1 基本用法</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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">worker</span><span class="params">(ctx context.Context, id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 收到取消信号，退出 (原因: %v)\n&quot;</span>, id, ctx.Err())</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 工作中...\n&quot;</span>, id)</span><br><span class="line">            time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</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">    ctx, cancel := context.WithCancel(context.Background())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> worker(ctx, <span class="number">1</span>)</span><br><span class="line">    <span class="keyword">go</span> worker(ctx, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 让 worker 工作 2 秒</span></span><br><span class="line">    time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取消所有 worker</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Main: 发送取消信号&quot;</span>)</span><br><span class="line">    cancel()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等一下让 worker 完成退出</span></span><br><span class="line">    time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">    fmt.Println(<span class="string">&quot;Main: 退出&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight nestedtext"><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></pre></td><td class="code"><pre><span class="line"><span class="attribute">Worker 1</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 2</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 1</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 2</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 2</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 1</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 1</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Worker 2</span><span class="punctuation">:</span> <span class="string">工作中...</span></span><br><span class="line"><span class="attribute">Main</span><span class="punctuation">:</span> <span class="string">发送取消信号</span></span><br><span class="line"><span class="attribute">Worker 2</span><span class="punctuation">:</span> <span class="string">收到取消信号，退出 (原因: context canceled)</span></span><br><span class="line"><span class="attribute">Worker 1</span><span class="punctuation">:</span> <span class="string">收到取消信号，退出 (原因: context canceled)</span></span><br><span class="line"><span class="attribute">Main</span><span class="punctuation">:</span> <span class="string">退出</span></span><br></pre></td></tr></table></figure><h3 id="4-2-工作原理">4.2 工作原理</h3><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">ctx, cancel := context.WithCancel(parent)</span><br></pre></td></tr></table></figure><ul><li><code>ctx</code>：派生的子 context，传给 goroutine</li><li><code>cancel</code>：取消函数，调用后 <code>ctx.Done()</code> 返回的 channel 被关闭</li><li>goroutine 通过 <code>select</code> + <code>&lt;-ctx.Done()</code> 监听取消信号</li></ul><figure class="highlight scss"><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="built_in">cancel</span>()</span><br><span class="line">  ↓</span><br><span class="line">ctx<span class="selector-class">.Done</span>() 的 channel 被关闭</span><br><span class="line">  ↓</span><br><span class="line">所有在 &lt;-ctx<span class="selector-class">.Done</span>() 上等待的 goroutine 被唤醒</span><br><span class="line">  ↓</span><br><span class="line">goroutine 检查 ctx<span class="selector-class">.Err</span>() 得知取消原因，执行清理后退出</span><br></pre></td></tr></table></figure><h3 id="4-3-cancel-必须调用">4.3 cancel 必须调用</h3><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">ctx, cancel := context.WithCancel(context.Background())</span><br><span class="line"><span class="keyword">defer</span> cancel()  <span class="comment">// 养成习惯：创建后立刻 defer cancel</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> doWork(ctx)</span><br></pre></td></tr></table></figure><p><strong>即使你不打算手动取消，也要 <code>defer cancel()</code></strong>。不调用 cancel 会导致 context 内部的资源（goroutine、timer 等）泄漏。Go 的 <code>vet</code> 工具会对此发出警告。</p><hr><h2 id="5-WithTimeout——超时自动取消">5. <code>WithTimeout</code>——超时自动取消</h2><h3 id="5-1-基本用法">5.1 基本用法</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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">slowOperation</span><span class="params">(ctx context.Context)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="comment">// 模拟一个耗时操作</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(<span class="number">3</span> * time.Second):</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;操作完成&quot;</span>, <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span>, ctx.Err()</span><br><span class="line">    &#125;</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="comment">// 最多等 2 秒</span></span><br><span class="line">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">2</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;开始操作...&quot;</span>)</span><br><span class="line">    result, err := slowOperation(ctx)</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;失败:&quot;</span>, err)  <span class="comment">// context deadline exceeded</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;成功:&quot;</span>, result)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight erlang"><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><br><span class="line">失败: context deadline exceeded</span><br></pre></td></tr></table></figure><p>操作需要 3 秒，但 context 只给了 2 秒。2 秒后 context 自动取消，<code>ctx.Done()</code> 的 channel 关闭，<code>slowOperation</code> 收到信号退出。</p><h3 id="5-2-WithTimeout-vs-WithDeadline">5.2 <code>WithTimeout</code> vs <code>WithDeadline</code></h3><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="comment">// WithTimeout：从现在起的相对时间</span></span><br><span class="line">ctx, cancel := context.WithTimeout(parent, <span class="number">5</span>*time.Second)</span><br><span class="line"><span class="comment">// 等价于</span></span><br><span class="line">ctx, cancel := context.WithDeadline(parent, time.Now().Add(<span class="number">5</span>*time.Second))</span><br></pre></td></tr></table></figure><ul><li><code>WithTimeout(parent, 5*time.Second)</code>：5 秒后取消</li><li><code>WithDeadline(parent, someTime)</code>：到达 someTime 时取消</li></ul><p><strong>大多数情况用 <code>WithTimeout</code></strong>，更直观。<code>WithDeadline</code> 适合需要精确时间点的场景。</p><h3 id="5-3-查看截止时间">5.3 查看截止时间</h3><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">ctx, cancel := context.WithTimeout(context.Background(), <span class="number">5</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">deadline, ok := ctx.Deadline()</span><br><span class="line"><span class="keyword">if</span> ok &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;截止时间:&quot;</span>, deadline.Format(<span class="string">&quot;15:04:05&quot;</span>))</span><br><span class="line">    fmt.Println(<span class="string">&quot;剩余时间:&quot;</span>, time.Until(deadline))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-4-子-context-不能延长父-context-的期限">5.4 子 context 不能延长父 context 的期限</h3><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"><span class="comment">// 父 context 2 秒超时</span></span><br><span class="line">parentCtx, parentCancel := context.WithTimeout(context.Background(), <span class="number">2</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> parentCancel()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 子 context 想要 5 秒——没用，最多 2 秒就会被父 context 取消</span></span><br><span class="line">childCtx, childCancel := context.WithTimeout(parentCtx, <span class="number">5</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> childCancel()</span><br><span class="line"></span><br><span class="line"><span class="comment">// childCtx 最多活 2 秒（受父 context 限制）</span></span><br></pre></td></tr></table></figure><p>子 context 的截止时间取<strong>父 context 和自己设置的较早者</strong>。你可以给子 context 更短的超时，但不能比父 context 更长。</p><hr><h2 id="6-ctx-Err-——取消的原因">6. <code>ctx.Err()</code>——取消的原因</h2><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">err := ctx.Err()</span><br></pre></td></tr></table></figure><table><thead><tr><th>返回值</th><th>含义</th></tr></thead><tbody><tr><td><code>nil</code></td><td>context 还没取消</td></tr><tr><td><code>context.Canceled</code></td><td>被手动调用 <code>cancel()</code> 取消</td></tr><tr><td><code>context.DeadlineExceeded</code></td><td>超时或到达截止时间</td></tr></tbody></table><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"><span class="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">    <span class="keyword">switch</span> ctx.Err() &#123;</span><br><span class="line">    <span class="keyword">case</span> context.Canceled:</span><br><span class="line">        fmt.Println(<span class="string">&quot;被手动取消&quot;</span>)</span><br><span class="line">    <span class="keyword">case</span> context.DeadlineExceeded:</span><br><span class="line">        fmt.Println(<span class="string">&quot;超时了&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="7-在-goroutine-中正确监听取消">7. 在 goroutine 中正确监听取消</h2><h3 id="7-1-标准模式">7.1 标准模式</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doWork</span><span class="params">(ctx context.Context)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            <span class="comment">// 收到取消信号，做清理然后退出</span></span><br><span class="line">            <span class="keyword">return</span> ctx.Err()</span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            <span class="comment">// 做一步工作</span></span><br><span class="line">            <span class="keyword">if</span> err := doOneStep(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> err</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></pre></td></tr></table></figure><h3 id="7-2-耗时操作中检查取消">7.2 耗时操作中检查取消</h3><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">processItems</span><span class="params">(ctx context.Context, items []<span class="type">string</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="comment">// 每处理一个就检查一次 context</span></span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Printf(<span class="string">&quot;在第 %d 个元素处被取消\n&quot;</span>, i)</span><br><span class="line">            <span class="keyword">return</span> ctx.Err()</span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 处理当前元素</span></span><br><span class="line">        process(item)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>每做一步就检查一次 <code>ctx.Done()</code></strong>，这样 goroutine 能尽快响应取消信号。如果你的操作是一个大循环，不要在循环外面检查——那样要等整个循环跑完才能响应取消。</p><h3 id="7-3-传递-context-给子操作">7.3 传递 context 给子操作</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handler</span><span class="params">(ctx context.Context)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="comment">// 把 ctx 传给每一个可能耗时的子操作</span></span><br><span class="line">    data, err := fetchData(ctx)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result, err := processData(ctx, data)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> saveResult(ctx, result)</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">fetchData</span><span class="params">(ctx context.Context)</span></span> ([]<span class="type">byte</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, ctx.Err()</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(<span class="number">2</span> * time.Second):</span><br><span class="line">        <span class="keyword">return</span> []<span class="type">byte</span>(<span class="string">&quot;data&quot;</span>), <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>context 像一条线，串起整个调用链。最顶层取消后，链上所有操作都能收到信号。</p><hr><h2 id="8-WithValue——通过-context-传值">8. <code>WithValue</code>——通过 context 传值</h2><h3 id="8-1-基本用法">8.1 基本用法</h3><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;context&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="keyword">type</span> contextKey <span class="type">string</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    keyRequestID contextKey = <span class="string">&quot;request_id&quot;</span></span><br><span class="line">    keyUserID    contextKey = <span class="string">&quot;user_id&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">handleRequest</span><span class="params">(ctx context.Context)</span></span> &#123;</span><br><span class="line">    reqID := ctx.Value(keyRequestID)</span><br><span class="line">    userID := ctx.Value(keyUserID)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;处理请求: reqID=%v, userID=%v\n&quot;</span>, reqID, userID)</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">    ctx := context.Background()</span><br><span class="line">    ctx = context.WithValue(ctx, keyRequestID, <span class="string">&quot;req-12345&quot;</span>)</span><br><span class="line">    ctx = context.WithValue(ctx, keyUserID, <span class="number">42</span>)</span><br><span class="line"></span><br><span class="line">    handleRequest(ctx)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">处理请求: <span class="attribute">reqID</span>=req-12345, <span class="attribute">userID</span>=42</span><br></pre></td></tr></table></figure><h3 id="8-2-key-的类型">8.2 key 的类型</h3><p><strong>不要用 <code>string</code> 或其他内置类型做 key</strong>。不同包可能用相同的 string，造成冲突。</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="comment">// 错误：可能和别的包冲突</span></span><br><span class="line">ctx = context.WithValue(ctx, <span class="string">&quot;user_id&quot;</span>, <span class="number">42</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：自定义类型做 key，包级别私有</span></span><br><span class="line"><span class="keyword">type</span> contextKey <span class="type">string</span></span><br><span class="line"><span class="keyword">const</span> keyUserID contextKey = <span class="string">&quot;user_id&quot;</span></span><br><span class="line">ctx = context.WithValue(ctx, keyUserID, <span class="number">42</span>)</span><br></pre></td></tr></table></figure><p>自定义类型即使底层是 <code>string</code>，也是不同的类型，不会和其他包的 key 冲突。</p><h3 id="8-3-什么该用-WithValue-传，什么不该">8.3 什么该用 WithValue 传，什么不该</h3><p><strong>该传的</strong>：请求级别的元数据（request ID、用户认证信息、追踪 ID）</p><p><strong>不该传的</strong>：函数的参数。如果一个值是函数运行必需的，应该作为参数显式传递，不要塞到 context 里。</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="comment">// 坏：把业务参数塞到 context 里</span></span><br><span class="line">ctx = context.WithValue(ctx, <span class="string">&quot;username&quot;</span>, <span class="string">&quot;alice&quot;</span>)</span><br><span class="line">processUser(ctx)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：业务参数显式传递</span></span><br><span class="line">processUser(ctx, <span class="string">&quot;alice&quot;</span>)</span><br></pre></td></tr></table></figure><p><strong>原则：context.Value 只用来传请求范围的横切关注点，不用来替代函数参数。</strong></p><hr><h2 id="9-实战示例">9. 实战示例</h2><h3 id="9-1-HTTP-请求超时">9.1 HTTP 请求超时</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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="comment">// 模拟 HTTP 请求</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">httpGet</span><span class="params">(ctx context.Context, url <span class="type">string</span>)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="comment">// 模拟网络延迟</span></span><br><span class="line">    delay := <span class="number">1</span>*time.Second + time.Duration(<span class="built_in">len</span>(url)%<span class="number">3</span>)*time.Second</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(delay):</span><br><span class="line">        <span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;响应 from %s (耗时 %v)&quot;</span>, url, delay), <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span>, fmt.Errorf(<span class="string">&quot;请求 %s 被取消: %w&quot;</span>, url, ctx.Err())</span><br><span class="line">    &#125;</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="comment">// 所有请求共享 3 秒的总超时</span></span><br><span class="line">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">3</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    urls := []<span class="type">string</span>&#123;</span><br><span class="line">        <span class="string">&quot;api.example.com/users&quot;</span>,</span><br><span class="line">        <span class="string">&quot;api.example.com/products&quot;</span>,</span><br><span class="line">        <span class="string">&quot;api.example.com/orders&quot;</span>,</span><br><span class="line">        <span class="string">&quot;api.example.com/inventory&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, url := <span class="keyword">range</span> urls &#123;</span><br><span class="line">        result, err := httpGet(ctx, url)</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;失败:&quot;</span>, err)</span><br><span class="line">            <span class="keyword">break</span>  <span class="comment">// 超时后后续请求也没必要了</span></span><br><span class="line">        &#125;</span><br><span class="line">        fmt.Println(<span class="string">&quot;成功:&quot;</span>, result)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-2-父子任务取消传播">9.2 父子任务取消传播</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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">subTask</span><span class="params">(ctx context.Context, name <span class="type">string</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; ; i++ &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  [%s] 第 %d 步被取消: %v\n&quot;</span>, name, i, ctx.Err())</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  [%s] 第 %d 步\n&quot;</span>, name, i)</span><br><span class="line">            time.Sleep(<span class="number">300</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</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">parentTask</span><span class="params">(ctx context.Context)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 从父 context 派生子 context，给子任务更短的超时</span></span><br><span class="line">    childCtx, childCancel := context.WithTimeout(ctx, <span class="number">1500</span>*time.Millisecond)</span><br><span class="line">    <span class="keyword">defer</span> childCancel()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    wg.Add(<span class="number">3</span>)</span><br><span class="line">    <span class="keyword">go</span> subTask(childCtx, <span class="string">&quot;子任务A&quot;</span>, &amp;wg)</span><br><span class="line">    <span class="keyword">go</span> subTask(childCtx, <span class="string">&quot;子任务B&quot;</span>, &amp;wg)</span><br><span class="line">    <span class="keyword">go</span> subTask(childCtx, <span class="string">&quot;子任务C&quot;</span>, &amp;wg)</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;父任务: 所有子任务已退出&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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">5</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;开始执行&quot;</span>)</span><br><span class="line">    parentTask(ctx)</span><br><span class="line">    fmt.Println(<span class="string">&quot;执行结束&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight angelscript"><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></pre></td><td class="code"><pre><span class="line">开始执行</span><br><span class="line"><span class="string">  [子任务C]</span> 第 <span class="number">1</span> 步</span><br><span class="line"><span class="string">  [子任务A]</span> 第 <span class="number">1</span> 步</span><br><span class="line"><span class="string">  [子任务B]</span> 第 <span class="number">1</span> 步</span><br><span class="line"><span class="string">  [子任务B]</span> 第 <span class="number">2</span> 步</span><br><span class="line"><span class="string">  [子任务C]</span> 第 <span class="number">2</span> 步</span><br><span class="line"><span class="string">  [子任务A]</span> 第 <span class="number">2</span> 步</span><br><span class="line"><span class="string">  [子任务A]</span> 第 <span class="number">3</span> 步</span><br><span class="line"><span class="string">  [子任务B]</span> 第 <span class="number">3</span> 步</span><br><span class="line"><span class="string">  [子任务C]</span> 第 <span class="number">3</span> 步</span><br><span class="line"><span class="string">  [子任务C]</span> 第 <span class="number">4</span> 步</span><br><span class="line"><span class="string">  [子任务A]</span> 第 <span class="number">4</span> 步</span><br><span class="line"><span class="string">  [子任务B]</span> 第 <span class="number">4</span> 步</span><br><span class="line"><span class="string">  [子任务B]</span> 第 <span class="number">5</span> 步被取消: context deadline exceeded</span><br><span class="line"><span class="string">  [子任务C]</span> 第 <span class="number">5</span> 步被取消: context deadline exceeded</span><br><span class="line"><span class="string">  [子任务A]</span> 第 <span class="number">5</span> 步被取消: context deadline exceeded</span><br><span class="line">父任务: 所有子任务已退出</span><br><span class="line">执行结束</span><br></pre></td></tr></table></figure><p>父 context 有 5 秒，但 <code>parentTask</code> 给子任务只分配了 1.5 秒。1.5 秒后子 context 超时，三个子任务同时收到取消信号退出。</p><h3 id="9-3-竞速请求——最快的响应胜出">9.3 竞速请求——最快的响应胜出</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;math/rand&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">queryServer</span><span class="params">(ctx context.Context, server <span class="type">string</span>, ch <span class="keyword">chan</span>&lt;- <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    delay := time.Duration(<span class="number">200</span>+rand.Intn(<span class="number">800</span>)) * time.Millisecond</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(delay):</span><br><span class="line">        ch &lt;- fmt.Sprintf(<span class="string">&quot;%s 响应 (耗时 %v)&quot;</span>, server, delay)</span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: 被取消，停止工作\n&quot;</span>, server)</span><br><span class="line">    &#125;</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">    ctx, cancel := context.WithCancel(context.Background())</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    result := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">    servers := []<span class="type">string</span>&#123;<span class="string">&quot;服务器A&quot;</span>, <span class="string">&quot;服务器B&quot;</span>, <span class="string">&quot;服务器C&quot;</span>&#125;</span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> servers &#123;</span><br><span class="line">        <span class="keyword">go</span> queryServer(ctx, s, result)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 只要最快的那个结果</span></span><br><span class="line">    fastest := &lt;-result</span><br><span class="line">    fmt.Println(<span class="string">&quot;最快响应:&quot;</span>, fastest)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取消其他还在运行的请求</span></span><br><span class="line">    cancel()</span><br><span class="line">    time.Sleep(<span class="number">100</span> * time.Millisecond)  <span class="comment">// 等一下看取消日志</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight nestedtext"><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="attribute">最快响应</span><span class="punctuation">:</span> <span class="string">服务器A 响应 (耗时 234ms)</span></span><br><span class="line">  <span class="attribute">服务器C</span><span class="punctuation">:</span> <span class="string">被取消，停止工作</span></span><br><span class="line">  <span class="attribute">服务器B</span><span class="punctuation">:</span> <span class="string">被取消，停止工作</span></span><br></pre></td></tr></table></figure><p>三个请求同时发出，第一个返回结果后立刻取消其他两个。这在分布式系统中很常见——冗余请求降低延迟。</p><h3 id="9-4-带取消的-Worker-Pool">9.4 带取消的 Worker Pool</h3><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></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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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">worker</span><span class="params">(ctx context.Context, id <span class="type">int</span>, jobs &lt;-<span class="keyword">chan</span> <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 收到取消，退出\n&quot;</span>, id)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">case</span> job, ok := &lt;-jobs:</span><br><span class="line">            <span class="keyword">if</span> !ok &#123;</span><br><span class="line">                fmt.Printf(<span class="string">&quot;Worker %d: 任务队列关闭，退出\n&quot;</span>, id)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 处理任务 %d\n&quot;</span>, id, job)</span><br><span class="line">            time.Sleep(<span class="number">200</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">2</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    jobs := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">20</span>)</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 3 个 worker</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> worker(ctx, i, jobs, &amp;wg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 发送 50 个任务（超时前可能处理不完）</span></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">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">50</span>; i++ &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> jobs &lt;- i:</span><br><span class="line">            <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">                fmt.Printf(<span class="string">&quot;发送方: 第 %d 个任务时超时，停止发送\n&quot;</span>, i)</span><br><span class="line">                <span class="built_in">close</span>(jobs)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(jobs)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;程序结束&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2 秒超时后，所有 worker 和发送方都会收到取消信号并退出。不会有 goroutine 泄漏。</p><hr><h2 id="10-context-的使用规范">10. context 的使用规范</h2><h3 id="10-1-函数签名约定">10.1 函数签名约定</h3><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="comment">// context 是第一个参数，命名为 ctx</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">DoSomething</span><span class="params">(ctx context.Context, arg1 <span class="type">string</span>, arg2 <span class="type">int</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>context 作为函数的<strong>第一个参数</strong></li><li>参数名叫 <strong><code>ctx</code></strong></li><li>不要把 context 放在结构体里</li></ul><h3 id="10-2-不要传-nil-context">10.2 不要传 nil context</h3><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="comment">// 错误</span></span><br><span class="line">DoSomething(<span class="literal">nil</span>, <span class="string">&quot;hello&quot;</span>, <span class="number">42</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：用 context.Background() 或 context.TODO()</span></span><br><span class="line">DoSomething(context.Background(), <span class="string">&quot;hello&quot;</span>, <span class="number">42</span>)</span><br></pre></td></tr></table></figure><h3 id="10-3-context-是不可变的">10.3 context 是不可变的</h3><p>每次 <code>WithCancel</code>、<code>WithTimeout</code>、<code>WithValue</code> 都返回一个<strong>新的</strong> context。原始 context 不会被修改。</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">ctx := context.Background()</span><br><span class="line">ctx2 := context.WithValue(ctx, key, <span class="string">&quot;value&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ctx 没有 key 这个值</span></span><br><span class="line"><span class="comment">// ctx2 有 key 这个值</span></span><br></pre></td></tr></table></figure><h3 id="10-4-cancel-函数可以多次调用">10.4 cancel 函数可以多次调用</h3><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">ctx, cancel := context.WithCancel(parent)</span><br><span class="line">cancel()  <span class="comment">// 第一次：取消 context</span></span><br><span class="line">cancel()  <span class="comment">// 第二次：安全，无操作</span></span><br><span class="line">cancel()  <span class="comment">// 第三次：安全，无操作</span></span><br></pre></td></tr></table></figure><p>多次调用 cancel 不会 panic，第一次之后的调用没有效果。所以 <code>defer cancel()</code> 是安全的。</p><h3 id="10-5-不要在结构体中存储-context">10.5 不要在结构体中存储 context</h3><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"><span class="comment">// 坏：context 存在结构体里</span></span><br><span class="line"><span class="keyword">type</span> Server <span class="keyword">struct</span> &#123;</span><br><span class="line">    ctx context.Context  <span class="comment">// 不要这样做</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：每次请求传入 context</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span></span> HandleRequest(ctx context.Context, req Request) Response &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>context 是<strong>请求级别</strong>的，每次请求应该有自己的 context。存在结构体里意味着所有请求共享一个 context，无法单独取消。</p><hr><h2 id="11-常见坑总结">11. 常见坑总结</h2><h3 id="11-1-忘记调用-cancel">11.1 忘记调用 cancel</h3><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="comment">// 每次调用都泄漏一点资源</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">bad</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, _ := context.WithTimeout(context.Background(), <span class="number">5</span>*time.Second)</span><br><span class="line">    doWork(ctx)</span><br><span class="line">    <span class="comment">// cancel 被忽略了，内部的 timer 无法释放</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">good</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">5</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line">    doWork(ctx)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-2-超时后没检查-context">11.2 超时后没检查 context</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 操作本身尊重 context 超时</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">query</span><span class="params">(ctx context.Context)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="comment">// 模拟查询</span></span><br><span class="line">    time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;result&quot;</span>, <span class="literal">nil</span>  <span class="comment">// 即使 ctx 超时了，这里也不知道</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：用 select 监听 ctx.Done()</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">query</span><span class="params">(ctx context.Context)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>, <span class="number">1</span>)</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">        time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">        ch &lt;- <span class="string">&quot;result&quot;</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> result := &lt;-ch:</span><br><span class="line">        <span class="keyword">return</span> result, <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;&quot;</span>, ctx.Err()</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-3-把-context-存在结构体里">11.3 把 context 存在结构体里</h3><p>前面规范里讲过，但再强调一遍。context 应该通过参数传递，不存在结构体中。</p><h3 id="11-4-用-WithValue-传业务参数">11.4 用 WithValue 传业务参数</h3><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="comment">// 坏：用 context 传递必要参数</span></span><br><span class="line">ctx = context.WithValue(ctx, <span class="string">&quot;name&quot;</span>, <span class="string">&quot;Alice&quot;</span>)</span><br><span class="line">greet(ctx)  <span class="comment">// 函数签名看不出需要 name</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：用函数参数</span></span><br><span class="line">greet(ctx, <span class="string">&quot;Alice&quot;</span>)  <span class="comment">// 一看就知道需要什么</span></span><br></pre></td></tr></table></figure><h3 id="11-5-阻塞操作不检查-context">11.5 阻塞操作不检查 context</h3><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="comment">// 如果 ch 一直没有数据，即使 ctx 超时也不会退出</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">bad</span><span class="params">(ctx context.Context, ch <span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    v := &lt;-ch  <span class="comment">// 不检查 ctx，可能永远阻塞</span></span><br><span class="line">    fmt.Println(v)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">good</span><span class="params">(ctx context.Context, ch <span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> v := &lt;-ch:</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        fmt.Println(<span class="string">&quot;超时退出&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>所有可能阻塞的操作都应该配合 <code>ctx.Done()</code> 使用。</strong> 这是 context 发挥作用的前提。</p><hr><h2 id="12-本课练习">12. 本课练习</h2><h3 id="练习-1：可取消的倒计时">练习 1：可取消的倒计时</h3><p>要求：</p><ul><li>实现一个 10 秒倒计时，每秒打印剩余时间</li><li>用一个 goroutine 监听用户输入，输入 “stop” 时取消倒计时</li><li>用 <code>context.WithCancel</code> 实现</li></ul><hr><h3 id="练习-2：多服务聚合查询">练习 2：多服务聚合查询</h3><p>要求：</p><ul><li>模拟 3 个服务（用户服务、订单服务、推荐服务），各自耗时不同</li><li>同时发起 3 个查询，总超时 2 秒</li><li>收集所有在超时前返回的结果</li><li>超时后打印哪些服务成功了，哪些超时了</li></ul><p>提示：用 <code>context.WithTimeout</code> + 多个 goroutine + channel 收集结果。</p><hr><h3 id="练习-3：分级超时">练习 3：分级超时</h3><p>要求：</p><ul><li>整体操作超时 5 秒</li><li>步骤 1（获取数据）超时 2 秒</li><li>步骤 2（处理数据）超时 2 秒</li><li>步骤 3（保存结果）超时 1 秒</li><li>每一步用子 context 设置独立超时</li></ul><hr><h3 id="练习-4：带-context-的-Worker-Pool">练习 4：带 context 的 Worker Pool</h3><p>要求：</p><ul><li>启动 3 个 worker goroutine</li><li>不断向它们分发任务</li><li>5 秒后通过 context 取消所有 worker</li><li>打印每个 worker 处理了多少个任务</li></ul><hr><h3 id="练习-5：请求追踪">练习 5：请求追踪</h3><p>要求：</p><ul><li>用 <code>context.WithValue</code> 在 context 中携带 request ID</li><li>实现 3 个函数：<code>handleRequest</code> → <code>queryDB</code> → <code>callAPI</code></li><li>每个函数从 context 中取出 request ID 并在日志中打印</li><li>模拟一个请求的完整链路</li></ul><hr><h2 id="13-自测题">13. 自测题</h2><h3 id="13-1-概念题">13.1 概念题</h3><ol><li><code>context</code> 解决的核心问题是什么？</li><li><code>context.Background()</code> 和 <code>context.TODO()</code> 有什么区别？</li><li><code>WithCancel</code>、<code>WithTimeout</code>、<code>WithDeadline</code> 各自的使用场景是什么？</li><li>父 context 取消后，子 context 会怎样？反过来呢？</li><li>子 context 能比父 context 有更长的超时时间吗？</li><li>为什么创建 context 后必须 <code>defer cancel()</code>？</li><li><code>ctx.Err()</code> 可能返回哪些值？分别代表什么？</li><li>为什么不应该把 context 存在结构体中？</li></ol><h3 id="13-2-代码阅读题">13.2 代码阅读题</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><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;context&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&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">    ctx, cancel := context.WithTimeout(context.Background(), <span class="number">3</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line">    childCtx, childCancel := context.WithTimeout(ctx, <span class="number">5</span>*time.Second)</span><br><span class="line">    <span class="keyword">defer</span> childCancel()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-childCtx.Done():</span><br><span class="line">        fmt.Println(<span class="string">&quot;子 context 结束:&quot;</span>, childCtx.Err())</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p>3 秒后输出：</p><figure class="highlight maxima"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">子 <span class="built_in">context</span> 结束: <span class="built_in">context</span> deadline exceeded</span><br></pre></td></tr></table></figure><p>解释：</p><ol><li>父 context 超时 3 秒</li><li>子 context 尝试设置 5 秒超时，但父 context 只有 3 秒</li><li>子 context 的实际超时 = min(父的 3 秒, 自己的 5 秒) = <strong>3 秒</strong></li><li>3 秒后父 context 超时，子 context 也被取消</li><li><code>childCtx.Err()</code> 返回 <code>context.DeadlineExceeded</code></li></ol><p><strong>子 context 不能比父 context 活得更久。</strong></p></details><hr><h2 id="14-本课总结">14. 本课总结</h2><p>这一课你学到了用 <code>context</code> 管理 goroutine 的生命周期。</p><table><thead><tr><th>工具</th><th>用途</th><th>关键点</th></tr></thead><tbody><tr><td><code>context.Background()</code></td><td>根 context</td><td>程序入口和顶层调用使用</td></tr><tr><td><code>WithCancel</code></td><td>手动取消</td><td>调用 cancel() 触发取消</td></tr><tr><td><code>WithTimeout</code></td><td>超时取消</td><td>指定持续时间</td></tr><tr><td><code>WithDeadline</code></td><td>截止时间取消</td><td>指定具体时间点</td></tr><tr><td><code>WithValue</code></td><td>传递值</td><td>只传请求级元数据，不替代函数参数</td></tr><tr><td><code>ctx.Done()</code></td><td>取消信号 channel</td><td>用 select 监听</td></tr><tr><td><code>ctx.Err()</code></td><td>取消原因</td><td><code>Canceled</code> 或 <code>DeadlineExceeded</code></td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong><code>context</code> 控制 goroutine 的生命周期——取消会从父 context 自动传播到所有子 context</strong></li><li><strong>创建 context 后立刻 <code>defer cancel()</code>——不调用会泄漏资源</strong></li><li><strong>所有可能阻塞的操作都用 <code>select</code> 配合 <code>ctx.Done()</code>——这是 context 生效的前提</strong></li></ol><hr><h2 id="15-下一课预告">15. 下一课预告</h2><p>到这里，你已经学完了 Go 并发编程的四大核心工具：goroutine、channel、select、context。下一课把它们组合起来，学习几个经典的<strong>并发设计模式</strong>。</p><p>下一课：<strong>并发模式实战</strong></p><p>会重点讲：</p><ul><li>生产者-消费者模式</li><li>Worker Pool（工作池）模式</li><li>Pipeline（流水线）模式</li><li>Fan-out / Fan-in（扇出/扇入）模式</li><li>怎么选择合适的并发模式</li></ul><p>学完下一课，你就能写出结构清晰、可维护的并发程序了。</p>]]></content>
    
    
    <summary type="html">Go语言系列29</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 28 课：同步原语</title>
    <link href="https://yjyrichard.github.io/posts/f7be9e2b.html"/>
    <id>https://yjyrichard.github.io/posts/f7be9e2b.html</id>
    <published>2026-03-22T15:28:11.728Z</published>
    <updated>2026-03-22T15:40:40.023Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 28 课：同步原语</h1><blockquote><p>学习定位：这是整套 Go 教程的第 28 课，也是阶段五（并发与工程阶段）的第四课。<br>前置要求：已经完成第 27 课，掌握了 <code>select</code> 与并发控制。需要理解 goroutine 和 channel 的基本使用。<br>本课目标：理解数据竞争问题，掌握 <code>sync.Mutex</code>、<code>sync.RWMutex</code>、<code>sync.Once</code> 等同步工具的使用，能用 <code>-race</code> 检测数据竞争，理解&quot;什么时候用锁、什么时候用 channel&quot;。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面三课你学了 goroutine、channel、select，已经能写不少并发程序了。但有一类问题还没解决：</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">counter := <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</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">        counter++  <span class="comment">// 1000 个 goroutine 同时修改同一个变量</span></span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你觉得最终 <code>counter</code> 是 1000？不一定。可能是 987，可能是 952，每次运行结果都不同。这就是<strong>数据竞争（data race）</strong>。</p><p>你需要搞明白以下问题：</p><ul><li>什么是数据竞争，为什么会发生</li><li>怎么用 <code>go run -race</code> 检测数据竞争</li><li><code>sync.Mutex</code> 怎么用——互斥锁</li><li><code>sync.RWMutex</code> 怎么用——读写锁</li><li><code>sync.Once</code> 怎么用——只执行一次</li><li><code>sync.WaitGroup</code> 的进阶用法（第 25 课初步介绍过）</li><li>什么时候用锁，什么时候用 channel</li></ul><hr><h2 id="2-数据竞争——并发编程的头号敌人">2. 数据竞争——并发编程的头号敌人</h2><h3 id="2-1-看一个-bug">2.1 看一个 bug</h3><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></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;sync&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">    counter := <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            counter++  <span class="comment">// 这一行有问题</span></span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;counter =&quot;</span>, counter)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行多次，你可能看到不同的结果：</p><figure class="highlight abnf"><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="attribute">counter</span> <span class="operator">=</span> <span class="number">962</span></span><br><span class="line"><span class="attribute">counter</span> <span class="operator">=</span> <span class="number">987</span></span><br><span class="line"><span class="attribute">counter</span> <span class="operator">=</span> <span class="number">1000</span></span><br><span class="line"><span class="attribute">counter</span> <span class="operator">=</span> <span class="number">951</span></span><br></pre></td></tr></table></figure><h3 id="2-2-为什么结果不对">2.2 为什么结果不对</h3><p><code>counter++</code> 看起来是一步操作，实际上是三步：</p><figure class="highlight apache"><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="attribute">1</span>. 读取 counter 的值（比如 <span class="number">42</span>）</span><br><span class="line"><span class="attribute">2</span>. 加 <span class="number">1</span>（得到 <span class="number">43</span>）</span><br><span class="line"><span class="attribute">3</span>. 写回 counter</span><br></pre></td></tr></table></figure><p>当两个 goroutine 同时执行 <code>counter++</code> 时：</p><figure class="highlight apache"><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="attribute">goroutine</span> A: 读取 counter = <span class="number">42</span></span><br><span class="line"><span class="attribute">goroutine</span> B: 读取 counter = <span class="number">42</span>    ← 读到了同样的值</span><br><span class="line"><span class="attribute">goroutine</span> A: 写入 counter = <span class="number">43</span></span><br><span class="line"><span class="attribute">goroutine</span> B: 写入 counter = <span class="number">43</span>    ← 覆盖了 A 的结果</span><br></pre></td></tr></table></figure><p>两个 goroutine 各自加了 1，但 counter 只从 42 变到了 43，丢了一次。这就是<strong>数据竞争</strong>。</p><h3 id="2-3-用-race-检测">2.3 用 <code>-race</code> 检测</h3><p>Go 内置了数据竞争检测器。只需要在运行或编译时加 <code>-race</code> 标志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go run -race main.go</span><br></pre></td></tr></table></figure><p>输出会包含类似这样的警告：</p><figure class="highlight asciidoc"><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></pre></td><td class="code"><pre><span class="line">==================</span><br><span class="line">WARNING: DATA RACE</span><br><span class="line">Read at 0x00c0000b4010 by goroutine 7:</span><br><span class="line">  main.main.func1()</span><br><span class="line">      /path/main.go:14 +0x38</span><br><span class="line"></span><br><span class="line">Previous write at 0x00c0000b4010 by goroutine 6:</span><br><span class="line">  main.main.func1()</span><br><span class="line">      /path/main.go:14 +0x4e</span><br><span class="line"></span><br><span class="line">Goroutine 7 (running) created at:</span><br><span class="line">  main.main()</span><br><span class="line">      /path/main.go:12 +0x6c</span><br><span class="line">==================</span><br></pre></td></tr></table></figure><p>它会精确告诉你哪一行代码存在数据竞争。<strong>养成习惯：开发和测试阶段加 <code>-race</code></strong>。</p><figure class="highlight bash"><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">go <span class="built_in">test</span> -race ./...    <span class="comment"># 测试时检测</span></span><br><span class="line">go build -race          <span class="comment"># 编译时开启（性能会降低，不要用于生产）</span></span><br></pre></td></tr></table></figure><hr><h2 id="3-sync-Mutex——互斥锁">3. <code>sync.Mutex</code>——互斥锁</h2><h3 id="3-1-什么是互斥锁">3.1 什么是互斥锁</h3><p><strong>互斥锁保证同一时间只有一个 goroutine 能访问被保护的资源。</strong></p><p>就像公共厕所的门锁：进去之前锁门（Lock），出来之后开锁（Unlock）。别人想进去就得等门开了才行。</p><h3 id="3-2-修复数据竞争">3.2 修复数据竞争</h3><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></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;sync&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">    counter := <span class="number">0</span></span><br><span class="line">    <span class="keyword">var</span> mu sync.Mutex</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line"></span><br><span class="line">            mu.Lock()    <span class="comment">// 加锁：其他 goroutine 必须等我解锁</span></span><br><span class="line">            counter++</span><br><span class="line">            mu.Unlock()  <span class="comment">// 解锁：其他 goroutine 可以进来了</span></span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;counter =&quot;</span>, counter)  <span class="comment">// 每次都是 1000</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在用 <code>go run -race main.go</code> 运行，不会再有竞争警告，结果每次都是 1000。</p><h3 id="3-3-Lock-和-Unlock-的规则">3.3 <code>Lock</code> 和 <code>Unlock</code> 的规则</h3><figure class="highlight stylus"><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="function"><span class="title">Lock</span><span class="params">()</span></span>    → 获取锁。如果锁已被占用，当前 goroutine 阻塞等待。</span><br><span class="line"><span class="function"><span class="title">Unlock</span><span class="params">()</span></span>  → 释放锁。如果没有持有锁就 Unlock，会 panic。</span><br></pre></td></tr></table></figure><h3 id="3-4-用-defer-确保解锁">3.4 用 <code>defer</code> 确保解锁</h3><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">mu.Lock()</span><br><span class="line"><span class="keyword">defer</span> mu.Unlock()  <span class="comment">// 无论如何都会解锁</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 做一些可能 return 或 panic 的操作</span></span><br><span class="line"><span class="keyword">if</span> condition &#123;</span><br><span class="line">    <span class="keyword">return</span>  <span class="comment">// defer 保证这里也会 Unlock</span></span><br><span class="line">&#125;</span><br><span class="line">doSomething()</span><br></pre></td></tr></table></figure><p><strong>永远用 <code>defer mu.Unlock()</code></strong>。如果忘了解锁，其他 goroutine 会永远等下去（死锁）。</p><h3 id="3-5-把锁和它保护的数据放在一起">3.5 把锁和它保护的数据放在一起</h3><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="comment">// 好的做法：把 Mutex 和它保护的数据封装在一个结构体里</span></span><br><span class="line"><span class="keyword">type</span> SafeCounter <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu    sync.Mutex</span><br><span class="line">    count <span class="type">int</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">(c *SafeCounter)</span></span> Increment() &#123;</span><br><span class="line">    c.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> c.mu.Unlock()</span><br><span class="line">    c.count++</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">(c *SafeCounter)</span></span> Value() <span class="type">int</span> &#123;</span><br><span class="line">    c.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> c.mu.Unlock()</span><br><span class="line">    <span class="keyword">return</span> c.count</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">    counter := &amp;SafeCounter&#123;&#125;</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            counter.Increment()</span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;counter =&quot;</span>, counter.Value())  <span class="comment">// 1000</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把锁和数据绑在一起，让使用者不需要关心加锁解锁，只需要调用方法。<strong>锁是数据的保镖，要跟着数据走。</strong></p><h3 id="3-6-并发安全的-map">3.6 并发安全的 map</h3><p>Go 的 map 不是并发安全的。多个 goroutine 同时读写 map 会直接 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></pre></td><td class="code"><pre><span class="line">m := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 多个 goroutine 同时写 map → panic: concurrent map writes</span></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123; m[<span class="string">&quot;a&quot;</span>] = <span class="number">1</span> &#125;()</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123; m[<span class="string">&quot;b&quot;</span>] = <span class="number">2</span> &#125;()</span><br></pre></td></tr></table></figure><p>用 Mutex 保护：</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">type</span> SafeMap <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu   sync.Mutex</span><br><span class="line">    data <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</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">NewSafeMap</span><span class="params">()</span></span> *SafeMap &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;SafeMap&#123;data: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)&#125;</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">(m *SafeMap)</span></span> Set(key <span class="type">string</span>, value <span class="type">int</span>) &#123;</span><br><span class="line">    m.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> m.mu.Unlock()</span><br><span class="line">    m.data[key] = value</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">(m *SafeMap)</span></span> Get(key <span class="type">string</span>) (<span class="type">int</span>, <span class="type">bool</span>) &#123;</span><br><span class="line">    m.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> m.mu.Unlock()</span><br><span class="line">    v, ok := m.data[key]</span><br><span class="line">    <span class="keyword">return</span> v, ok</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">(m *SafeMap)</span></span> Delete(key <span class="type">string</span>) &#123;</span><br><span class="line">    m.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> m.mu.Unlock()</span><br><span class="line">    <span class="built_in">delete</span>(m.data, key)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="4-sync-RWMutex——读写锁">4. <code>sync.RWMutex</code>——读写锁</h2><h3 id="4-1-问题：读多写少的场景">4.1 问题：读多写少的场景</h3><p>普通 Mutex 有个缺点：不管读还是写，都要排队。但很多时候，读操作远多于写操作（比如缓存）。多个读操作之间不会冲突，不需要互斥。</p><h3 id="4-2-RWMutex-的规则">4.2 RWMutex 的规则</h3><p><code>sync.RWMutex</code> 区分读锁和写锁：</p><table><thead><tr><th>操作</th><th>方法</th><th>规则</th></tr></thead><tbody><tr><td>读锁</td><td><code>RLock()</code> / <code>RUnlock()</code></td><td>多个读可以同时持有</td></tr><tr><td>写锁</td><td><code>Lock()</code> / <code>Unlock()</code></td><td>独占，读和写都要等</td></tr></tbody></table><figure class="highlight"><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><br><span class="line">写锁和读锁互斥    → 写阻塞读，读也阻塞写</span><br><span class="line">写锁和写锁互斥    → 写阻塞写</span><br></pre></td></tr></table></figure><h3 id="4-3-使用示例">4.3 使用示例</h3><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><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></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;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="keyword">type</span> Cache <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu   sync.RWMutex</span><br><span class="line">    data <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</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">NewCache</span><span class="params">()</span></span> *Cache &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;Cache&#123;data: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">string</span>)&#125;</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">(c *Cache)</span></span> Get(key <span class="type">string</span>) (<span class="type">string</span>, <span class="type">bool</span>) &#123;</span><br><span class="line">    c.mu.RLock()         <span class="comment">// 读锁：多个 goroutine 可以同时读</span></span><br><span class="line">    <span class="keyword">defer</span> c.mu.RUnlock()</span><br><span class="line">    v, ok := c.data[key]</span><br><span class="line">    <span class="keyword">return</span> v, ok</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">(c *Cache)</span></span> Set(key, value <span class="type">string</span>) &#123;</span><br><span class="line">    c.mu.Lock()          <span class="comment">// 写锁：独占</span></span><br><span class="line">    <span class="keyword">defer</span> c.mu.Unlock()</span><br><span class="line">    c.data[key] = value</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">    cache := NewCache()</span><br><span class="line">    cache.Set(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;Go&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 10 个读 goroutine</span></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">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> j := <span class="number">0</span>; j &lt; <span class="number">100</span>; j++ &#123;</span><br><span class="line">                <span class="keyword">if</span> v, ok := cache.Get(<span class="string">&quot;name&quot;</span>); ok &#123;</span><br><span class="line">                    _ = v  <span class="comment">// 使用数据</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;读者 %d 完成\n&quot;</span>, id)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 2 个写 goroutine</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">2</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> j := <span class="number">0</span>; j &lt; <span class="number">10</span>; j++ &#123;</span><br><span class="line">                cache.Set(<span class="string">&quot;name&quot;</span>, fmt.Sprintf(<span class="string">&quot;Go_%d_%d&quot;</span>, id, j))</span><br><span class="line">                time.Sleep(<span class="number">10</span> * time.Millisecond)</span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;写者 %d 完成\n&quot;</span>, id)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>10 个读 goroutine 可以同时读，互不阻塞。只有写操作会独占锁。</p><h3 id="4-4-什么时候用-RWMutex">4.4 什么时候用 RWMutex</h3><ul><li>读操作远多于写操作 → 用 <code>RWMutex</code>（读不互斥，性能更好）</li><li>读写频率差不多 → 用 <code>Mutex</code>（RWMutex 本身有额外开销）</li><li>只有写操作 → 用 <code>Mutex</code></li></ul><p><strong>简单规则：不确定就用 <code>Mutex</code>，确认读多写少再换 <code>RWMutex</code>。</strong></p><hr><h2 id="5-sync-Once——只执行一次">5. <code>sync.Once</code>——只执行一次</h2><h3 id="5-1-场景：初始化只做一次">5.1 场景：初始化只做一次</h3><p>有些操作只应该执行一次，比如初始化数据库连接、加载配置文件。即使多个 goroutine 同时调用，也只执行第一次。</p><h3 id="5-2-基本用法">5.2 基本用法</h3><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></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;sync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> once sync.Once</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">initialize</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;初始化...（只会打印一次）&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">worker</span><span class="params">(id <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 尝试初始化\n&quot;</span>, id)</span><br><span class="line">    once.Do(initialize)  <span class="comment">// 只有第一个到达的 goroutine 会执行 initialize</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 继续工作\n&quot;</span>, id)</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">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> worker(i, &amp;wg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight tap"><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">Worker<span class="number"> 3 </span>尝试初始化</span><br><span class="line">初始化...（只会打印一次）</span><br><span class="line">Worker<span class="number"> 3 </span>继续工作</span><br><span class="line">Worker<span class="number"> 1 </span>尝试初始化</span><br><span class="line">Worker<span class="number"> 1 </span>继续工作</span><br><span class="line">Worker<span class="number"> 5 </span>尝试初始化</span><br><span class="line">Worker<span class="number"> 5 </span>继续工作</span><br><span class="line">Worker<span class="number"> 2 </span>尝试初始化</span><br><span class="line">Worker<span class="number"> 2 </span>继续工作</span><br><span class="line">Worker<span class="number"> 4 </span>尝试初始化</span><br><span class="line">Worker<span class="number"> 4 </span>继续工作</span><br></pre></td></tr></table></figure><p>“初始化…” 只打印了一次。其他 goroutine 调用 <code>once.Do</code> 时会等第一次执行完毕后直接返回。</p><h3 id="5-3-单例模式">5.3 单例模式</h3><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> Database <span class="keyword">struct</span> &#123;</span><br><span class="line">    host <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> (</span><br><span class="line">    dbInstance *Database</span><br><span class="line">    dbOnce     sync.Once</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">GetDB</span><span class="params">()</span></span> *Database &#123;</span><br><span class="line">    dbOnce.Do(<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;创建数据库连接&quot;</span>)</span><br><span class="line">        dbInstance = &amp;Database&#123;host: <span class="string">&quot;localhost:5432&quot;</span>&#125;</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> dbInstance</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">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 多个 goroutine 同时获取数据库实例</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            db := GetDB()</span><br><span class="line">            fmt.Println(<span class="string">&quot;使用数据库:&quot;</span>, db.host)</span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不管多少个 goroutine 调用 <code>GetDB()</code>，数据库连接只会创建一次。</p><h3 id="5-4-Once-的特性">5.4 Once 的特性</h3><ul><li><code>Do</code> 方法接收一个无参数无返回值的函数</li><li>只执行第一次调用传入的函数，后续调用直接返回</li><li>即使第一次执行的函数 panic 了，后续调用也不会再执行（认为&quot;已经执行过了&quot;）</li><li>一个 <code>Once</code> 只能配一个操作，不能重置</li></ul><hr><h2 id="6-sync-WaitGroup-进阶">6. <code>sync.WaitGroup</code> 进阶</h2><p>第 25 课初步介绍了 WaitGroup。这里补充几个进阶用法。</p><h3 id="6-1-一次性-Add">6.1 一次性 Add</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">n := <span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式一：循环中每次 Add(1)</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line">    wg.Add(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">go</span> worker(i, &amp;wg)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方式二：一次性 Add(n)（更清晰）</span></span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line">    <span class="keyword">go</span> worker(i, &amp;wg)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两种都可以。方式二更直观——一眼就能看出总共有 n 个任务。但要确保 <code>Add</code> 的数量和实际启动的 goroutine 数量一致。</p><h3 id="6-2-嵌套-WaitGroup">6.2 嵌套 WaitGroup</h3><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></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;sync&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="keyword">var</span> outerWg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> stage := <span class="number">1</span>; stage &lt;= <span class="number">3</span>; stage++ &#123;</span><br><span class="line">        outerWg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(s <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> outerWg.Done()</span><br><span class="line"></span><br><span class="line">            <span class="keyword">var</span> innerWg sync.WaitGroup</span><br><span class="line">            <span class="keyword">for</span> task := <span class="number">1</span>; task &lt;= <span class="number">3</span>; task++ &#123;</span><br><span class="line">                innerWg.Add(<span class="number">1</span>)</span><br><span class="line">                <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(t <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">                    <span class="keyword">defer</span> innerWg.Done()</span><br><span class="line">                    fmt.Printf(<span class="string">&quot;阶段 %d - 任务 %d 完成\n&quot;</span>, s, t)</span><br><span class="line">                &#125;(task)</span><br><span class="line">            &#125;</span><br><span class="line">            innerWg.Wait()</span><br><span class="line">            fmt.Printf(<span class="string">&quot;=== 阶段 %d 全部完成 ===\n&quot;</span>, s)</span><br><span class="line">        &#125;(stage)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    outerWg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;所有阶段完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>外层 WaitGroup 等所有阶段完成，每个阶段内部有自己的 WaitGroup 等所有任务完成。</p><hr><h2 id="7-sync-Map——并发安全的-map">7. <code>sync.Map</code>——并发安全的 map</h2><h3 id="7-1-标准库提供的并发-map">7.1 标准库提供的并发 map</h3><p>Go 标准库提供了 <code>sync.Map</code>，不需要自己加锁：</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></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;sync&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="keyword">var</span> m sync.Map</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 并发写入</span></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">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            m.Store(fmt.Sprintf(<span class="string">&quot;key_%d&quot;</span>, n), n*n)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读取</span></span><br><span class="line">    m.Range(<span class="function"><span class="keyword">func</span><span class="params">(key, value any)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%s = %v\n&quot;</span>, key, value)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>  <span class="comment">// 返回 true 继续遍历，false 停止</span></span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 单个读取</span></span><br><span class="line">    <span class="keyword">if</span> v, ok := m.Load(<span class="string">&quot;key_5&quot;</span>); ok &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;key_5 =&quot;</span>, v)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 删除</span></span><br><span class="line">    m.Delete(<span class="string">&quot;key_3&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// LoadOrStore：存在就返回旧值，不存在就存入新值</span></span><br><span class="line">    actual, loaded := m.LoadOrStore(<span class="string">&quot;key_5&quot;</span>, <span class="number">999</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;key_5:&quot;</span>, actual, <span class="string">&quot;已存在:&quot;</span>, loaded)  <span class="comment">// key_5: 25 已存在: true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-2-sync-Map-的-API">7.2 sync.Map 的 API</h3><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td><code>Store(key, value)</code></td><td>存入键值对</td></tr><tr><td><code>Load(key)</code></td><td>读取，返回 <code>(value, ok)</code></td></tr><tr><td><code>Delete(key)</code></td><td>删除</td></tr><tr><td><code>LoadOrStore(key, value)</code></td><td>存在返回旧值，不存在存入新值</td></tr><tr><td><code>LoadAndDelete(key)</code></td><td>读取并删除</td></tr><tr><td><code>Range(func)</code></td><td>遍历所有键值对</td></tr></tbody></table><h3 id="7-3-sync-Map-vs-自己加锁的-map">7.3 sync.Map vs 自己加锁的 map</h3><table><thead><tr><th>特性</th><th><code>sync.Map</code></th><th><code>Mutex</code> + <code>map</code></th></tr></thead><tbody><tr><td>使用难度</td><td>简单，不用管锁</td><td>需要自己加锁解锁</td></tr><tr><td>类型安全</td><td>不安全（key 和 value 都是 <code>any</code>）</td><td>安全（有具体类型）</td></tr><tr><td>适用场景</td><td>key 相对固定、读多写少</td><td>通用场景</td></tr><tr><td>性能</td><td>特定场景更好</td><td>一般场景更稳定</td></tr></tbody></table><p><strong>选择建议</strong>：</p><ul><li>大多数情况用 <code>Mutex</code> + 普通 <code>map</code>（类型安全、更灵活）</li><li>两种特殊场景用 <code>sync.Map</code>：key 只增不删、各 goroutine 读写不同的 key</li></ul><hr><h2 id="8-锁-vs-channel——怎么选">8. 锁 vs channel——怎么选</h2><p>这是 Go 并发编程中最常见的疑问。</p><h3 id="8-1-用-channel-的场景">8.1 用 channel 的场景</h3><ul><li>goroutine 之间<strong>传递数据</strong></li><li>协调多个 goroutine 的<strong>执行顺序</strong></li><li>流水线、生产者-消费者模式</li><li>通知和信号</li></ul><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="comment">// 适合 channel：数据从一个 goroutine 流向另一个</span></span><br><span class="line">results := <span class="built_in">make</span>(<span class="keyword">chan</span> Result)</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123; results &lt;- compute() &#125;()</span><br><span class="line">r := &lt;-results</span><br></pre></td></tr></table></figure><h3 id="8-2-用锁的场景">8.2 用锁的场景</h3><ul><li><strong>保护共享状态</strong>（计数器、缓存、连接池）</li><li>多个 goroutine 读写<strong>同一个数据结构</strong></li><li>简单的互斥访问</li></ul><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="comment">// 适合锁：多个 goroutine 操作同一个计数器</span></span><br><span class="line">mu.Lock()</span><br><span class="line">counter++</span><br><span class="line">mu.Unlock()</span><br></pre></td></tr></table></figure><h3 id="8-3-一句话总结">8.3 一句话总结</h3><blockquote><p><strong>传递数据用 channel，保护数据用锁。</strong></p></blockquote><table><thead><tr><th>场景</th><th>选择</th><th>原因</th></tr></thead><tbody><tr><td>goroutine 间传递数据</td><td>channel</td><td>数据在&quot;流动&quot;</td></tr><tr><td>保护共享的计数器</td><td>Mutex</td><td>数据在&quot;原地修改&quot;</td></tr><tr><td>保护共享的 map</td><td>Mutex / sync.Map</td><td>数据在&quot;原地修改&quot;</td></tr><tr><td>通知某件事完成</td><td>channel</td><td>这是一种通信</td></tr><tr><td>只执行一次初始化</td><td>sync.Once</td><td>专用工具</td></tr><tr><td>等待一组 goroutine</td><td>WaitGroup</td><td>专用工具</td></tr></tbody></table><hr><h2 id="9-实战示例">9. 实战示例</h2><h3 id="9-1-并发安全的银行账户">9.1 并发安全的银行账户</h3><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><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></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;sync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Account <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu      sync.Mutex</span><br><span class="line">    balance <span class="type">int</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">NewAccount</span><span class="params">(initial <span class="type">int</span>)</span></span> *Account &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;Account&#123;balance: initial&#125;</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">(a *Account)</span></span> Deposit(amount <span class="type">int</span>) &#123;</span><br><span class="line">    a.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> a.mu.Unlock()</span><br><span class="line">    a.balance += amount</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  存入 %d, 余额: %d\n&quot;</span>, amount, a.balance)</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">(a *Account)</span></span> Withdraw(amount <span class="type">int</span>) <span class="type">bool</span> &#123;</span><br><span class="line">    a.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> a.mu.Unlock()</span><br><span class="line">    <span class="keyword">if</span> a.balance &lt; amount &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  取款 %d 失败，余额不足: %d\n&quot;</span>, amount, a.balance)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">    a.balance -= amount</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  取出 %d, 余额: %d\n&quot;</span>, amount, a.balance)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</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">(a *Account)</span></span> Balance() <span class="type">int</span> &#123;</span><br><span class="line">    a.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> a.mu.Unlock()</span><br><span class="line">    <span class="keyword">return</span> a.balance</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">    account := NewAccount(<span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟多人同时操作同一个账户</span></span><br><span class="line">    operations := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        op     <span class="type">string</span></span><br><span class="line">        amount <span class="type">int</span></span><br><span class="line">    &#125;&#123;</span><br><span class="line">        &#123;<span class="string">&quot;deposit&quot;</span>, <span class="number">200</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;withdraw&quot;</span>, <span class="number">500</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;deposit&quot;</span>, <span class="number">300</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;withdraw&quot;</span>, <span class="number">800</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;withdraw&quot;</span>, <span class="number">100</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;deposit&quot;</span>, <span class="number">150</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, op := <span class="keyword">range</span> operations &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(action <span class="type">string</span>, amount <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">switch</span> action &#123;</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&quot;deposit&quot;</span>:</span><br><span class="line">                account.Deposit(amount)</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&quot;withdraw&quot;</span>:</span><br><span class="line">                account.Withdraw(amount)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;(op.op, op.amount)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n最终余额: %d\n&quot;</span>, account.Balance())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个操作都加了锁，保证余额计算不会被打乱。</p><h3 id="9-2-并发安全的访问计数器">9.2 并发安全的访问计数器</h3><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><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></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;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="keyword">type</span> PageCounter <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu     sync.RWMutex</span><br><span class="line">    counts <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</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">NewPageCounter</span><span class="params">()</span></span> *PageCounter &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;PageCounter&#123;counts: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)&#125;</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">(pc *PageCounter)</span></span> Visit(page <span class="type">string</span>) &#123;</span><br><span class="line">    pc.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> pc.mu.Unlock()</span><br><span class="line">    pc.counts[page]++</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">(pc *PageCounter)</span></span> GetCount(page <span class="type">string</span>) <span class="type">int</span> &#123;</span><br><span class="line">    pc.mu.RLock()</span><br><span class="line">    <span class="keyword">defer</span> pc.mu.RUnlock()</span><br><span class="line">    <span class="keyword">return</span> pc.counts[page]</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">(pc *PageCounter)</span></span> TopPages(n <span class="type">int</span>) []<span class="type">string</span> &#123;</span><br><span class="line">    pc.mu.RLock()</span><br><span class="line">    <span class="keyword">defer</span> pc.mu.RUnlock()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集所有页面</span></span><br><span class="line">    <span class="keyword">type</span> kv <span class="keyword">struct</span> &#123;</span><br><span class="line">        page  <span class="type">string</span></span><br><span class="line">        count <span class="type">int</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> pages []kv</span><br><span class="line">    <span class="keyword">for</span> p, c := <span class="keyword">range</span> pc.counts &#123;</span><br><span class="line">        pages = <span class="built_in">append</span>(pages, kv&#123;p, c&#125;)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 按访问量排序（第 23 课）</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(pages)<span class="number">-1</span>; i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := i + <span class="number">1</span>; j &lt; <span class="built_in">len</span>(pages); j++ &#123;</span><br><span class="line">            <span class="keyword">if</span> pages[j].count &gt; pages[i].count &#123;</span><br><span class="line">                pages[i], pages[j] = pages[j], pages[i]</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result := []<span class="type">string</span>&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n &amp;&amp; i &lt; <span class="built_in">len</span>(pages); i++ &#123;</span><br><span class="line">        result = <span class="built_in">append</span>(result, fmt.Sprintf(<span class="string">&quot;%s: %d次&quot;</span>, pages[i].page, pages[i].count))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    counter := NewPageCounter()</span><br><span class="line">    pages := []<span class="type">string</span>&#123;<span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/about&quot;</span>, <span class="string">&quot;/api/users&quot;</span>, <span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/products&quot;</span>, <span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/about&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟并发访问</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">100</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            page := pages[i%<span class="built_in">len</span>(pages)]</span><br><span class="line">            counter.Visit(page)</span><br><span class="line">        &#125;(<span class="comment">// 注意这里没有传 i，闭包直接捕获了 i</span></span><br><span class="line">        )</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 上面的代码有闭包 bug！修正版本：</span></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 打印结果</span></span><br><span class="line">    <span class="keyword">for</span> _, line := <span class="keyword">range</span> counter.TopPages(<span class="number">5</span>) &#123;</span><br><span class="line">        fmt.Println(line)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>等一下——上面的代码有闭包陷阱（第 25 课）。让我用正确的版本：</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></pre></td><td class="code"><pre><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">    counter := NewPageCounter()</span><br><span class="line">    pages := []<span class="type">string</span>&#123;<span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/about&quot;</span>, <span class="string">&quot;/api/users&quot;</span>, <span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/products&quot;</span>, <span class="string">&quot;/home&quot;</span>, <span class="string">&quot;/about&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">100</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(idx <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            page := pages[idx%<span class="built_in">len</span>(pages)]</span><br><span class="line">            counter.Visit(page)</span><br><span class="line">            time.Sleep(time.Millisecond) <span class="comment">// 模拟请求耗时</span></span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 页面访问排行 ===&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, line := <span class="keyword">range</span> counter.TopPages(<span class="number">5</span>) &#123;</span><br><span class="line">        fmt.Println(line)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用了 <code>RWMutex</code>：<code>Visit</code> 用写锁（修改数据），<code>GetCount</code> 和 <code>TopPages</code> 用读锁（只读数据）。多个读请求可以同时执行。</p><h3 id="9-3-用-Once-实现懒加载配置">9.3 用 Once 实现懒加载配置</h3><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></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;encoding/json&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;sync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Config <span class="keyword">struct</span> &#123;</span><br><span class="line">    AppName <span class="type">string</span> <span class="string">`json:&quot;app_name&quot;`</span></span><br><span class="line">    Port    <span class="type">int</span>    <span class="string">`json:&quot;port&quot;`</span></span><br><span class="line">    Debug   <span class="type">bool</span>   <span class="string">`json:&quot;debug&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> (</span><br><span class="line">    config     *Config</span><br><span class="line">    configOnce sync.Once</span><br><span class="line">    configErr  <span class="type">error</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">GetConfig</span><span class="params">()</span></span> (*Config, <span class="type">error</span>) &#123;</span><br><span class="line">    configOnce.Do(<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;加载配置文件...（只执行一次）&quot;</span>)</span><br><span class="line"></span><br><span class="line">        data, err := os.ReadFile(<span class="string">&quot;config.json&quot;</span>)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            configErr = fmt.Errorf(<span class="string">&quot;读取配置失败: %w&quot;</span>, err)</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">        config = &amp;Config&#123;&#125;</span><br><span class="line">        <span class="keyword">if</span> err := json.Unmarshal(data, config); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            configErr = fmt.Errorf(<span class="string">&quot;解析配置失败: %w&quot;</span>, err)</span><br><span class="line">            config = <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> config, configErr</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">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            cfg, err := GetConfig()</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;Worker %d: 配置加载失败: %v\n&quot;</span>, id, err)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;Worker %d: 使用配置 %s:%d\n&quot;</span>, id, cfg.AppName, cfg.Port)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置只加载一次，后续所有调用直接返回缓存的结果。</p><hr><h2 id="10-常见坑总结">10. 常见坑总结</h2><h3 id="10-1-忘记解锁——死锁">10.1 忘记解锁——死锁</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">bad</span><span class="params">()</span></span> &#123;</span><br><span class="line">    mu.Lock()</span><br><span class="line">    <span class="keyword">if</span> someCondition &#123;</span><br><span class="line">        <span class="keyword">return</span>  <span class="comment">// 忘了 Unlock！其他 goroutine 永远进不来</span></span><br><span class="line">    &#125;</span><br><span class="line">    mu.Unlock()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 解决：用 defer</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">good</span><span class="params">()</span></span> &#123;</span><br><span class="line">    mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> mu.Unlock()</span><br><span class="line">    <span class="keyword">if</span> someCondition &#123;</span><br><span class="line">        <span class="keyword">return</span>  <span class="comment">// defer 会自动 Unlock</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-2-重复加锁——死锁">10.2 重复加锁——死锁</h3><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="function"><span class="keyword">func</span> <span class="title">doublelock</span><span class="params">()</span></span> &#123;</span><br><span class="line">    mu.Lock()</span><br><span class="line">    mu.Lock()  <span class="comment">// 同一个 goroutine 再次 Lock → 死锁！</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Go 的 <code>sync.Mutex</code> 不是可重入锁。同一个 goroutine 对同一个 Mutex 加锁两次会死锁。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 常见的隐蔽场景</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SafeData)</span></span> A() &#123;</span><br><span class="line">    s.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> s.mu.Unlock()</span><br><span class="line">    s.B()  <span class="comment">// B 里面也加锁了！</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">(s *SafeData)</span></span> B() &#123;</span><br><span class="line">    s.mu.Lock()  <span class="comment">// 死锁！A 已经持有锁了</span></span><br><span class="line">    <span class="keyword">defer</span> s.mu.Unlock()</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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><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="params">(s *SafeData)</span></span> A() &#123;</span><br><span class="line">    s.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> s.mu.Unlock()</span><br><span class="line">    s.b()  <span class="comment">// 调用不加锁的版本</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">(s *SafeData)</span></span> B() &#123;</span><br><span class="line">    s.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> s.mu.Unlock()</span><br><span class="line">    s.b()</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">(s *SafeData)</span></span> b() &#123;</span><br><span class="line">    <span class="comment">// 实际逻辑，不加锁（调用方负责加锁）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-3-锁的粒度太大">10.3 锁的粒度太大</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 坏：整个操作都在锁内，包括耗时的 IO 操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">bad</span><span class="params">(mu *sync.Mutex)</span></span> &#123;</span><br><span class="line">    mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> mu.Unlock()</span><br><span class="line"></span><br><span class="line">    data := fetchFromNetwork()   <span class="comment">// 网络请求，可能要几秒</span></span><br><span class="line">    result := processData(data)  <span class="comment">// 数据处理</span></span><br><span class="line">    saveResult(result)           <span class="comment">// 保存结果</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 好：只锁需要保护的部分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">good</span><span class="params">(mu *sync.Mutex)</span></span> &#123;</span><br><span class="line">    data := fetchFromNetwork()   <span class="comment">// 不需要锁</span></span><br><span class="line">    result := processData(data)  <span class="comment">// 不需要锁</span></span><br><span class="line"></span><br><span class="line">    mu.Lock()</span><br><span class="line">    saveResult(result)           <span class="comment">// 只有这步需要锁</span></span><br><span class="line">    mu.Unlock()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>锁的持有时间越短越好。</strong> 持有锁的时候做耗时操作，会让其他 goroutine 白白等待。</p><h3 id="10-4-复制-Mutex">10.4 复制 Mutex</h3><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="keyword">type</span> Data <span class="keyword">struct</span> &#123;</span><br><span class="line">    mu sync.Mutex</span><br><span class="line">    v  <span class="type">int</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">modify</span><span class="params">(d Data)</span></span> &#123;  <span class="comment">// 值传递！mu 被复制了</span></span><br><span class="line">    d.mu.Lock()         <span class="comment">// 锁的是副本，原始的 mu 没被锁</span></span><br><span class="line">    <span class="keyword">defer</span> d.mu.Unlock()</span><br><span class="line">    d.v++</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>sync.Mutex</code>、<code>sync.WaitGroup</code> 等同步原语<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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">modify</span><span class="params">(d *Data)</span></span> &#123;  <span class="comment">// 指针传递</span></span><br><span class="line">    d.mu.Lock()</span><br><span class="line">    <span class="keyword">defer</span> d.mu.Unlock()</span><br><span class="line">    d.v++</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-5-读写锁中用错了锁类型">10.5 读写锁中用错了锁类型</h3><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="comment">// 写操作却用了读锁——没有保护效果！</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span></span> Set(key, value <span class="type">string</span>) &#123;</span><br><span class="line">    c.mu.RLock()           <span class="comment">// 应该用 Lock()</span></span><br><span class="line">    <span class="keyword">defer</span> c.mu.RUnlock()</span><br><span class="line">    c.data[key] = value    <span class="comment">// 写操作没有被保护</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>读操作用 <code>RLock</code>/<code>RUnlock</code>，写操作用 <code>Lock</code>/<code>Unlock</code>。搞反了就失去了保护。</p><hr><h2 id="11-本课练习">11. 本课练习</h2><h3 id="练习-1：并发安全的计数器">练习 1：并发安全的计数器</h3><p>要求：</p><ul><li>实现一个 <code>Counter</code> 结构体，支持 <code>Add(n)</code>、<code>Sub(n)</code>、<code>Value()</code> 方法</li><li>所有方法并发安全</li><li>启动 100 个 goroutine 各 Add(1)，再启动 50 个各 Sub(1)</li><li>最终值应该是 50</li></ul><hr><h3 id="练习-2：并发安全的排行榜">练习 2：并发安全的排行榜</h3><p>要求：</p><ul><li>实现一个 <code>Leaderboard</code> 结构体，支持 <code>AddScore(name, score)</code> 和 <code>Top(n)</code> 方法</li><li><code>AddScore</code> 累加分数</li><li><code>Top(n)</code> 返回前 n 名（按分数降序）</li><li>用 <code>RWMutex</code> 保护：<code>AddScore</code> 用写锁，<code>Top</code> 用读锁</li><li>启动多个 goroutine 并发添加分数和查询排名</li></ul><hr><h3 id="练习-3：限流器">练习 3：限流器</h3><p>要求：</p><ul><li>实现一个 <code>RateLimiter</code> 结构体，限制每秒最多处理 N 个请求</li><li>用 <code>Mutex</code> + 时间戳数组实现</li><li><code>Allow()</code> 方法返回 <code>bool</code>，表示当前请求是否被允许</li></ul><p>提示：记录最近 N 个请求的时间戳，新请求来时检查最早那个是否在 1 秒内。</p><hr><h3 id="练习-4：数据竞争检测练习">练习 4：数据竞争检测练习</h3><p>要求：</p><ul><li>写一段<strong>故意有数据竞争</strong>的代码</li><li>用 <code>go run -race</code> 运行，观察输出</li><li>用 Mutex 修复，再用 <code>-race</code> 确认修复成功</li></ul><hr><h3 id="练习-5：读写锁性能对比">练习 5：读写锁性能对比</h3><p>要求：</p><ul><li>分别用 <code>Mutex</code> 和 <code>RWMutex</code> 保护一个 map</li><li>模拟 90% 读 + 10% 写的场景</li><li>用 <code>time.Since</code> 测量两种方式的耗时</li><li>对比结果</li></ul><hr><h2 id="12-自测题">12. 自测题</h2><h3 id="12-1-概念题">12.1 概念题</h3><ol><li>什么是数据竞争？<code>counter++</code> 为什么不是并发安全的？</li><li><code>go run -race</code> 做了什么？应该在什么阶段使用？</li><li><code>sync.Mutex</code> 的 <code>Lock</code> 和 <code>Unlock</code> 分别做什么？</li><li>为什么总应该用 <code>defer mu.Unlock()</code> 而不是手动调用 <code>Unlock()</code>？</li><li><code>sync.RWMutex</code> 和 <code>sync.Mutex</code> 有什么区别？什么场景用 <code>RWMutex</code>？</li><li><code>sync.Once</code> 的 <code>Do</code> 方法被调用多次会怎样？</li><li>Go 的 <code>sync.Mutex</code> 是可重入的吗？同一个 goroutine 连续 Lock 两次会怎样？</li><li>&quot;传递数据用 channel，保护数据用锁&quot;这句话怎么理解？</li></ol><h3 id="12-2-代码阅读题">12.2 代码阅读题</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><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></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;sync&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">    data := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br><span class="line">    <span class="keyword">var</span> mu sync.Mutex</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            key := fmt.Sprintf(<span class="string">&quot;key_%d&quot;</span>, n)</span><br><span class="line"></span><br><span class="line">            mu.Lock()</span><br><span class="line">            data[key] = n * n</span><br><span class="line">            mu.Unlock()</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> k, v := <span class="keyword">range</span> data &#123;</span><br><span class="line">        fmt.Println(k, v)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><p><strong>没有数据竞争。</strong></p><p>解释：</p><ol><li>所有对 <code>data</code> 的写操作都在 <code>mu.Lock()</code> 和 <code>mu.Unlock()</code> 之间——受锁保护</li><li><code>wg.Wait()</code> 确保所有写操作完成后才开始读（<code>for range</code>）</li><li>主 goroutine 中的 <code>for range</code> 是在所有 goroutine 结束后执行的，此时没有并发写入</li><li>闭包变量 <code>i</code> 通过参数 <code>n</code> 传入，每个 goroutine 有自己的副本</li></ol><p>这段代码是安全的。用 <code>go run -race</code> 运行不会报任何警告。</p></details><hr><h2 id="13-本课总结">13. 本课总结</h2><p>这一课你学到了如何在并发环境中保护共享数据。</p><table><thead><tr><th>工具</th><th>用途</th><th>关键点</th></tr></thead><tbody><tr><td><code>sync.Mutex</code></td><td>互斥锁</td><td>同一时间只有一个 goroutine 能访问</td></tr><tr><td><code>sync.RWMutex</code></td><td>读写锁</td><td>多读不互斥，写独占</td></tr><tr><td><code>sync.Once</code></td><td>只执行一次</td><td>初始化、单例</td></tr><tr><td><code>sync.WaitGroup</code></td><td>等待一组 goroutine</td><td>Add → Done → Wait</td></tr><tr><td><code>sync.Map</code></td><td>并发安全 map</td><td>API 是 <code>any</code> 类型，少数场景适用</td></tr><tr><td><code>-race</code></td><td>数据竞争检测</td><td>开发测试阶段必用</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>多个 goroutine 读写同一个变量就有数据竞争——用 Mutex 保护，用 <code>-race</code> 检测</strong></li><li><strong><code>defer mu.Unlock()</code> 是铁律——防止忘记解锁导致死锁</strong></li><li><strong>传递数据用 channel，保护数据用锁——两者互补，不要只用一种</strong></li></ol><hr><h2 id="14-下一课预告">14. 下一课预告</h2><p>你现在能创建 goroutine、用 channel 通信、用 select 多路复用、用锁保护共享数据。但还有一个重要的问题：<strong>怎么取消一个正在运行的 goroutine？</strong></p><p>想象一个场景：你发起了一个 HTTP 请求，但用户取消了操作。请求的 goroutine 还在傻傻等待响应。你需要一种机制告诉它&quot;不用干了，回来吧&quot;。</p><p>下一课：<strong><code>context</code> 与取消机制</strong></p><p>会重点讲：</p><ul><li><code>context</code> 是什么——goroutine 的生命周期管理器</li><li><code>context.Background()</code> 和 <code>context.TODO()</code></li><li><code>context.WithCancel</code>——手动取消</li><li><code>context.WithTimeout</code> 和 <code>context.WithDeadline</code>——自动超时取消</li><li>怎么在 goroutine 中监听取消信号</li><li>context 的传递约定</li></ul><p>学完下一课，你就能优雅地管理 goroutine 的生命周期了。</p>]]></content>
    
    
    <summary type="html">Go语言系列28</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 27 课：select 与并发控制</title>
    <link href="https://yjyrichard.github.io/posts/2a70c22.html"/>
    <id>https://yjyrichard.github.io/posts/2a70c22.html</id>
    <published>2026-03-22T15:28:11.725Z</published>
    <updated>2026-03-22T15:40:40.021Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 27 课：<code>select</code> 与并发控制</h1><blockquote><p>学习定位：这是整套 Go 教程的第 27 课，也是阶段五（并发与工程阶段）的第三课。<br>前置要求：已经完成第 26 课，掌握了 channel 的创建、发送、接收和关闭。<br>本课目标：掌握 <code>select</code> 的语法和工作机制，能同时监听多个 channel，能实现超时控制和非阻塞操作，理解常见的 <code>select</code> 使用模式。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>上一课你学会了用 channel 在两个 goroutine 之间传递数据。但真实的并发场景不会这么简单：</p><ul><li>你要<strong>同时等待</strong>用户输入和超时信号——哪个先来就处理哪个</li><li>你要<strong>同时监听</strong>多个服务的响应——任何一个返回就继续</li><li>你要做<strong>非阻塞</strong>的 channel 操作——有数据就收，没有就跳过</li></ul><p>这些场景都需要<strong>同时监听多个 channel</strong>，这就是 <code>select</code> 干的事。</p><p>你可以把 <code>select</code> 理解为<strong>channel 版的 <code>switch</code></strong>：<code>switch</code> 是根据值选择分支，<code>select</code> 是根据哪个 channel 准备好了来选择分支。</p><p>你需要搞明白以下问题：</p><ul><li><code>select</code> 的基本语法</li><li>多个 case 同时就绪时怎么选</li><li>怎么用 <code>default</code> 做非阻塞操作</li><li>怎么实现超时控制</li><li>怎么实现定时任务</li><li>常见的 <code>select</code> 使用模式</li></ul><hr><h2 id="2-select-基本语法">2. <code>select</code> 基本语法</h2><h3 id="2-1-长什么样">2.1 长什么样</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> v := &lt;-ch1:</span><br><span class="line">    <span class="comment">// ch1 有数据可接收时执行</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;从 ch1 收到:&quot;</span>, v)</span><br><span class="line"><span class="keyword">case</span> ch2 &lt;- value:</span><br><span class="line">    <span class="comment">// ch2 可以发送数据时执行</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;发送到 ch2&quot;</span>)</span><br><span class="line"><span class="keyword">case</span> v, ok := &lt;-ch3:</span><br><span class="line">    <span class="comment">// ch3 有数据或已关闭时执行</span></span><br><span class="line">    <span class="keyword">if</span> !ok &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;ch3 已关闭&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">    <span class="comment">// 所有 case 都不就绪时执行</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;没有 channel 准备好&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-2-和-switch-的区别">2.2 和 <code>switch</code> 的区别</h3><table><thead><tr><th>特性</th><th><code>switch</code></th><th><code>select</code></th></tr></thead><tbody><tr><td>选择依据</td><td>值匹配</td><td>channel 就绪状态</td></tr><tr><td>case 内容</td><td>表达式</td><td>channel 操作（发送或接收）</td></tr><tr><td>执行方式</td><td>从上到下匹配第一个</td><td><strong>随机</strong>选择一个就绪的 case</td></tr><tr><td>阻塞行为</td><td>不阻塞</td><td>没有 default 时会阻塞</td></tr></tbody></table><p><strong>最关键的区别</strong>：<code>switch</code> 按顺序匹配，<code>select</code> 随机选择。</p><hr><h2 id="3-第一个-select-示例">3. 第一个 <code>select</code> 示例</h2><h3 id="3-1-同时监听两个-channel">3.1 同时监听两个 channel</h3><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></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;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">    ch1 := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line">    ch2 := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line"></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">        time.Sleep(<span class="number">1</span> * time.Second)</span><br><span class="line">        ch1 &lt;- <span class="string">&quot;来自 ch1&quot;</span></span><br><span class="line">    &#125;()</span><br><span class="line"></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">        time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">        ch2 &lt;- <span class="string">&quot;来自 ch2&quot;</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等待第一个返回的结果</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch1:</span><br><span class="line">        fmt.Println(<span class="string">&quot;收到:&quot;</span>, msg)</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch2:</span><br><span class="line">        fmt.Println(<span class="string">&quot;收到:&quot;</span>, msg)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">收到: 来自 ch1</span></span><br></pre></td></tr></table></figure><p>ch1 在 1 秒后就绪，ch2 要 2 秒。<code>select</code> 会选择先就绪的那个 case，所以选了 ch1。</p><p><strong>注意</strong>：<code>select</code> 只执行一个 case 就结束了。如果你想持续监听，需要把 <code>select</code> 放在循环里。</p><h3 id="3-2-多个-case-同时就绪——随机选择">3.2 多个 case 同时就绪——随机选择</h3><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></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">    ch1 := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>, <span class="number">1</span>)</span><br><span class="line">    ch2 := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    ch1 &lt;- <span class="string">&quot;A&quot;</span></span><br><span class="line">    ch2 &lt;- <span class="string">&quot;B&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 两个 case 都就绪了，select 会随机选一个</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch1:</span><br><span class="line">        fmt.Println(<span class="string">&quot;选了 ch1:&quot;</span>, msg)</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch2:</span><br><span class="line">        fmt.Println(<span class="string">&quot;选了 ch2:&quot;</span>, msg)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行多次，你会看到有时选 ch1，有时选 ch2。<strong>这是故意设计的</strong>——随机选择防止某个 channel 被饿死（永远得不到处理）。</p><hr><h2 id="4-select-在循环中使用">4. <code>select</code> 在循环中使用</h2><h3 id="4-1-持续监听多个-channel">4.1 持续监听多个 channel</h3><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></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;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">    tick := time.Tick(<span class="number">500</span> * time.Millisecond)   <span class="comment">// 每 500ms 触发一次</span></span><br><span class="line">    boom := time.After(<span class="number">2</span> * time.Second)          <span class="comment">// 2 秒后触发一次</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-tick:</span><br><span class="line">            fmt.Println(<span class="string">&quot;tick.&quot;</span>)</span><br><span class="line">        <span class="keyword">case</span> &lt;-boom:</span><br><span class="line">            fmt.Println(<span class="string">&quot;BOOM!&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span>  <span class="comment">// 退出整个程序</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Println(<span class="string">&quot;    .&quot;</span>)</span><br><span class="line">            time.Sleep(<span class="number">100</span> * time.Millisecond)</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>输出：</p><figure class="highlight erlang"><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></pre></td><td class="code"><pre><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">tick.</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">tick.</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">tick.</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">    .</span><br><span class="line">tick.</span><br><span class="line">BOOM!</span><br></pre></td></tr></table></figure><p>这个例子展示了 <code>select</code> 的三种 case 类型：</p><ul><li><strong>定时触发</strong>：<code>time.Tick</code> 返回一个 channel，定期发送信号</li><li><strong>延时触发</strong>：<code>time.After</code> 返回一个 channel，指定时间后发送一次信号</li><li><strong>默认分支</strong>：没有 channel 就绪时执行</li></ul><h3 id="4-2-监听多个数据源">4.2 监听多个数据源</h3><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></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;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">sensor</span><span class="params">(name <span class="type">string</span>, interval time.Duration, ch <span class="keyword">chan</span>&lt;- <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        time.Sleep(interval)</span><br><span class="line">        ch &lt;- fmt.Sprintf(<span class="string">&quot;[%s] 数据 #%d&quot;</span>, name, i)</span><br><span class="line">    &#125;</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">    tempCh := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line">    humidityCh := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> sensor(<span class="string">&quot;温度&quot;</span>, <span class="number">300</span>*time.Millisecond, tempCh)</span><br><span class="line">    <span class="keyword">go</span> sensor(<span class="string">&quot;湿度&quot;</span>, <span class="number">500</span>*time.Millisecond, humidityCh)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">    wg.Add(<span class="number">1</span>)</span><br><span class="line"></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">        <span class="keyword">defer</span> wg.Done()</span><br><span class="line">        timeout := time.After(<span class="number">2</span> * time.Second)</span><br><span class="line">        <span class="keyword">for</span> &#123;</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> msg := &lt;-tempCh:</span><br><span class="line">                fmt.Println(<span class="string">&quot;收到:&quot;</span>, msg)</span><br><span class="line">            <span class="keyword">case</span> msg := &lt;-humidityCh:</span><br><span class="line">                fmt.Println(<span class="string">&quot;收到:&quot;</span>, msg)</span><br><span class="line">            <span class="keyword">case</span> &lt;-timeout:</span><br><span class="line">                fmt.Println(<span class="string">&quot;监听超时，退出&quot;</span>)</span><br><span class="line">                <span class="keyword">return</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight less"><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="selector-attr">[温度]</span> 数据 <span class="selector-id">#1</span></span><br><span class="line">收到: <span class="selector-attr">[湿度]</span> 数据 <span class="selector-id">#1</span></span><br><span class="line">收到: <span class="selector-attr">[温度]</span> 数据 <span class="selector-id">#2</span></span><br><span class="line">收到: <span class="selector-attr">[温度]</span> 数据 <span class="selector-id">#3</span></span><br><span class="line">收到: <span class="selector-attr">[湿度]</span> 数据 <span class="selector-id">#2</span></span><br><span class="line">收到: <span class="selector-attr">[湿度]</span> 数据 <span class="selector-id">#3</span></span><br><span class="line">监听超时，退出</span><br></pre></td></tr></table></figure><p>两个传感器以不同频率产生数据，<code>select</code> 不管谁先来，来了就处理。</p><hr><h2 id="5-超时控制">5. 超时控制</h2><h3 id="5-1-time-After——最常用的超时方式">5.1 <code>time.After</code>——最常用的超时方式</h3><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></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;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">slowOperation</span><span class="params">()</span></span> <span class="keyword">chan</span> <span class="type">string</span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</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">        time.Sleep(<span class="number">3</span> * time.Second)  <span class="comment">// 模拟一个很慢的操作</span></span><br><span class="line">        ch &lt;- <span class="string">&quot;操作完成&quot;</span></span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> ch</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">    result := slowOperation()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-result:</span><br><span class="line">        fmt.Println(<span class="string">&quot;成功:&quot;</span>, msg)</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(<span class="number">2</span> * time.Second):</span><br><span class="line">        fmt.Println(<span class="string">&quot;超时！操作太慢了&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">超时！操作太慢了</span><br></pre></td></tr></table></figure><p>操作需要 3 秒，但我们只等 2 秒。<code>time.After(2 * time.Second)</code> 返回一个 channel，2 秒后会发送一个时间值。<code>select</code> 谁先就绪就执行谁——超时先到，就走超时分支。</p><h3 id="5-2-time-After-的原理">5.2 <code>time.After</code> 的原理</h3><p><code>time.After(d)</code> 做了这些事：</p><ol><li>创建一个 <code>chan time.Time</code></li><li>启动一个内部定时器</li><li>d 时间后往 channel 发送当前时间</li><li>返回这个 channel</li></ol><p>所以 <code>&lt;-time.After(2 * time.Second)</code> 本质上就是&quot;等 2 秒后收到一个信号&quot;。</p><h3 id="5-3-每次操作独立超时">5.3 每次操作独立超时</h3><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="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;math/rand&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">query</span><span class="params">(server <span class="type">string</span>)</span></span> <span class="keyword">chan</span> <span class="type">string</span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</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">        <span class="comment">// 模拟响应时间 100ms ~ 600ms</span></span><br><span class="line">        delay := time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">500</span>)) * time.Millisecond</span><br><span class="line">        time.Sleep(delay)</span><br><span class="line">        ch &lt;- fmt.Sprintf(<span class="string">&quot;%s: 响应耗时 %v&quot;</span>, server, delay)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> ch</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">    servers := []<span class="type">string</span>&#123;<span class="string">&quot;服务器A&quot;</span>, <span class="string">&quot;服务器B&quot;</span>, <span class="string">&quot;服务器C&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, server := <span class="keyword">range</span> servers &#123;</span><br><span class="line">        result := query(server)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> msg := &lt;-result:</span><br><span class="line">            fmt.Println(<span class="string">&quot;成功 -&quot;</span>, msg)</span><br><span class="line">        <span class="keyword">case</span> &lt;-time.After(<span class="number">300</span> * time.Millisecond):</span><br><span class="line">            fmt.Printf(<span class="string">&quot;超时 - %s 没有在 300ms 内响应\n&quot;</span>, server)</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>可能的输出：</p><figure class="highlight less"><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="selector-tag">-</span> 服务器<span class="selector-tag">A</span>: 响应耗时 <span class="number">156ms</span></span><br><span class="line">超时 <span class="selector-tag">-</span> 服务器<span class="selector-tag">B</span> 没有在 <span class="number">300ms</span> 内响应</span><br><span class="line">成功 <span class="selector-tag">-</span> 服务器<span class="selector-tag">C</span>: 响应耗时 <span class="number">234ms</span></span><br></pre></td></tr></table></figure><p>每个请求有独立的 300ms 超时限制。</p><hr><h2 id="6-default-分支——非阻塞操作">6. <code>default</code> 分支——非阻塞操作</h2><h3 id="6-1-没有-default-时">6.1 没有 <code>default</code> 时</h3><p>如果所有 case 都没有就绪，<code>select</code> 会<strong>阻塞等待</strong>，直到某个 case 就绪。</p><h3 id="6-2-有-default-时">6.2 有 <code>default</code> 时</h3><p>如果加了 <code>default</code>，所有 case 都没就绪时直接执行 <code>default</code>，<strong>不会阻塞</strong>。</p><h3 id="6-3-非阻塞接收">6.3 非阻塞接收</h3><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 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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 非阻塞接收：有数据就收，没有就跳过</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> v := &lt;-ch:</span><br><span class="line">        fmt.Println(<span class="string">&quot;收到:&quot;</span>, v)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;没有数据可接收&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ch &lt;- <span class="number">42</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 现在有数据了</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> v := &lt;-ch:</span><br><span class="line">        fmt.Println(<span class="string">&quot;收到:&quot;</span>, v)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;没有数据可接收&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight makefile"><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><br><span class="line"><span class="section">收到: 42</span></span><br></pre></td></tr></table></figure><h3 id="6-4-非阻塞发送">6.4 非阻塞发送</h3><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">1</span>)</span><br><span class="line">    ch &lt;- <span class="number">1</span>  <span class="comment">// 缓冲区满了</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 非阻塞发送：能发就发，不能发就跳过</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> ch &lt;- <span class="number">2</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;发送成功&quot;</span>)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        fmt.Println(<span class="string">&quot;缓冲区满了，发送失败&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">缓冲区满了，发送失败</span><br></pre></td></tr></table></figure><h3 id="6-5-什么时候用-default">6.5 什么时候用 <code>default</code></h3><ul><li><strong>轮询</strong>：循环中不断检查 channel，有就处理，没有就做其他事</li><li><strong>避免阻塞</strong>：尝试发送或接收，但不想被卡住</li><li><strong>忙等待</strong>：通常配合 <code>time.Sleep</code>，防止 CPU 空转</li></ul><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="comment">// 轮询模式</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        handle(msg)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="comment">// 没有消息，做其他事或短暂休息</span></span><br><span class="line">        time.Sleep(<span class="number">10</span> * time.Millisecond)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>注意</strong>：<code>default</code> 用不好会导致 CPU 空转（一直循环做 default，浪费 CPU）。如果不需要非阻塞，就不要加 <code>default</code>。</p><hr><h2 id="7-常见使用模式">7. 常见使用模式</h2><h3 id="7-1-模式一：超时退出">7.1 模式一：超时退出</h3><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="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> result := &lt;-ch:</span><br><span class="line">    fmt.Println(<span class="string">&quot;结果:&quot;</span>, result)</span><br><span class="line"><span class="keyword">case</span> &lt;-time.After(<span class="number">5</span> * time.Second):</span><br><span class="line">    fmt.Println(<span class="string">&quot;超时&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最常见的模式。等待结果，但不无限等。</p><h3 id="7-2-模式二：定时任务">7.2 模式二：定时任务</h3><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></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;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">    ticker := time.NewTicker(<span class="number">1</span> * time.Second)</span><br><span class="line">    <span class="keyword">defer</span> ticker.Stop()  <span class="comment">// 不用了要 Stop，防止泄漏</span></span><br><span class="line"></span><br><span class="line">    done := time.After(<span class="number">5</span> * time.Second)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> t := &lt;-ticker.C:</span><br><span class="line">            fmt.Println(<span class="string">&quot;定时任务执行:&quot;</span>, t.Format(<span class="string">&quot;15:04:05&quot;</span>))</span><br><span class="line">        <span class="keyword">case</span> &lt;-done:</span><br><span class="line">            fmt.Println(<span class="string">&quot;结束&quot;</span>)</span><br><span class="line">            <span class="keyword">return</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>输出：</p><figure class="highlight makefile"><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="section">定时任务执行: 14:30:01</span></span><br><span class="line"><span class="section">定时任务执行: 14:30:02</span></span><br><span class="line"><span class="section">定时任务执行: 14:30:03</span></span><br><span class="line"><span class="section">定时任务执行: 14:30:04</span></span><br><span class="line"><span class="section">定时任务执行: 14:30:05</span></span><br><span class="line">结束</span><br></pre></td></tr></table></figure><p><strong><code>time.NewTicker</code> vs <code>time.Tick</code></strong>：</p><ul><li><code>time.NewTicker</code> 返回一个 <code>*Ticker</code>，可以调 <code>Stop()</code> 停止，用完必须停止</li><li><code>time.Tick</code> 是简写版，返回的 channel 无法停止，会一直占用资源</li><li>在生产代码中用 <code>NewTicker</code>，在简单示例中可以用 <code>Tick</code></li></ul><h3 id="7-3-模式三：退出信号">7.3 模式三：退出信号</h3><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></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;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">worker</span><span class="params">(quit &lt;-<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-quit:</span><br><span class="line">            fmt.Println(<span class="string">&quot;Worker: 收到退出信号，清理中...&quot;</span>)</span><br><span class="line">            <span class="comment">// 做清理工作</span></span><br><span class="line">            fmt.Println(<span class="string">&quot;Worker: 已退出&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Println(<span class="string">&quot;Worker: 工作中...&quot;</span>)</span><br><span class="line">            time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</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">    quit := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> worker(quit)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 让 worker 工作 2 秒</span></span><br><span class="line">    time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 发送退出信号</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Main: 发送退出信号&quot;</span>)</span><br><span class="line">    <span class="built_in">close</span>(quit)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等一下让 worker 完成清理</span></span><br><span class="line">    time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight avrasm"><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="symbol">Worker:</span> 工作中...</span><br><span class="line"><span class="symbol">Worker:</span> 工作中...</span><br><span class="line"><span class="symbol">Worker:</span> 工作中...</span><br><span class="line"><span class="symbol">Worker:</span> 工作中...</span><br><span class="line"><span class="symbol">Main:</span> 发送退出信号</span><br><span class="line"><span class="symbol">Worker:</span> 收到退出信号，清理中...</span><br><span class="line"><span class="symbol">Worker:</span> 已退出</span><br></pre></td></tr></table></figure><p>用 <code>close(quit)</code> 发送退出信号。关闭 channel 后，<code>&lt;-quit</code> 立即返回零值（上一课讲的），所以所有在 <code>&lt;-quit</code> 上等待的 goroutine 都会被唤醒。</p><h3 id="7-4-模式四：多个服务竞速">7.4 模式四：多个服务竞速</h3><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></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;math/rand&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">queryServer</span><span class="params">(server <span class="type">string</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">string</span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</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">        delay := time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">900</span>)) * time.Millisecond</span><br><span class="line">        time.Sleep(delay)</span><br><span class="line">        ch &lt;- fmt.Sprintf(<span class="string">&quot;%s (耗时 %v)&quot;</span>, server, delay)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> ch</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="comment">// 同时请求三个服务器，谁先响应用谁的结果</span></span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> result := &lt;-queryServer(<span class="string">&quot;主服务器&quot;</span>):</span><br><span class="line">        fmt.Println(<span class="string">&quot;使用:&quot;</span>, result)</span><br><span class="line">    <span class="keyword">case</span> result := &lt;-queryServer(<span class="string">&quot;备用服务器1&quot;</span>):</span><br><span class="line">        fmt.Println(<span class="string">&quot;使用:&quot;</span>, result)</span><br><span class="line">    <span class="keyword">case</span> result := &lt;-queryServer(<span class="string">&quot;备用服务器2&quot;</span>):</span><br><span class="line">        fmt.Println(<span class="string">&quot;使用:&quot;</span>, result)</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(<span class="number">1</span> * time.Second):</span><br><span class="line">        fmt.Println(<span class="string">&quot;所有服务器都超时了&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时发出三个请求，谁先响应就用谁的结果。这在分布式系统中很常见——通过冗余请求降低延迟。</p><h3 id="7-5-模式五：心跳检测">7.5 模式五：心跳检测</h3><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></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;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">startWorker</span><span class="params">(heartbeat <span class="keyword">chan</span>&lt;- <span class="keyword">struct</span>&#123;&#125;)</span></span> &#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">        <span class="keyword">for</span> i := <span class="number">0</span>; ; i++ &#123;</span><br><span class="line">            <span class="comment">// 每 500ms 发送一次心跳</span></span><br><span class="line">            time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">            <span class="keyword">select</span> &#123;</span><br><span class="line">            <span class="keyword">case</span> heartbeat &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;:</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="comment">// 心跳 channel 满了就跳过，不阻塞</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 模拟：第 6 次后 worker &quot;卡住&quot; 了</span></span><br><span class="line">            <span class="keyword">if</span> i == <span class="number">5</span> &#123;</span><br><span class="line">                fmt.Println(<span class="string">&quot;Worker: 我卡住了...&quot;</span>)</span><br><span class="line">                time.Sleep(<span class="number">10</span> * time.Second)</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"></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">    heartbeat := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;, <span class="number">1</span>)</span><br><span class="line">    startWorker(heartbeat)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-heartbeat:</span><br><span class="line">            fmt.Println(<span class="string">&quot;收到心跳，Worker 正常&quot;</span>)</span><br><span class="line">        <span class="keyword">case</span> &lt;-time.After(<span class="number">2</span> * time.Second):</span><br><span class="line">            fmt.Println(<span class="string">&quot;超过 2 秒没有心跳，Worker 可能挂了!&quot;</span>)</span><br><span class="line">            <span class="keyword">return</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>输出：</p><figure class="highlight erlang-repl"><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">收到心跳，Worker 正常</span><br><span class="line">收到心跳，Worker 正常</span><br><span class="line">收到心跳，Worker 正常</span><br><span class="line">收到心跳，Worker 正常</span><br><span class="line">收到心跳，Worker 正常</span><br><span class="line">收到心跳，Worker 正常</span><br><span class="line">超过 <span class="number">2</span> 秒没有心跳，Worker 可能挂了!</span><br></pre></td></tr></table></figure><p>Worker 定期发送心跳，监控方用 <code>select</code> + <code>time.After</code> 检测。如果超过 2 秒没有心跳，就判定 Worker 异常。</p><hr><h2 id="8-空-select">8. 空 <code>select</code></h2><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="keyword">select</span> &#123;&#125;  <span class="comment">// 永久阻塞</span></span><br></pre></td></tr></table></figure><p>空的 <code>select</code> 没有任何 case，所以永远不会有就绪的 case，主 goroutine 会<strong>永久阻塞</strong>。</p><p>什么时候用？当你的所有逻辑都在 goroutine 中运行，主 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"><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">go</span> server1()</span><br><span class="line">    <span class="keyword">go</span> server2()</span><br><span class="line">    <span class="keyword">go</span> server3()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">select</span> &#123;&#125;  <span class="comment">// 主 goroutine 永远等待，让其他 goroutine 运行</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>不过在生产环境中，通常用信号监听（比如等待 Ctrl+C）来代替空 select。</p><hr><h2 id="9-select-的注意事项">9. <code>select</code> 的注意事项</h2><h3 id="9-1-select-不会-fall-through">9.1 <code>select</code> 不会 fall through</h3><p>和 <code>switch</code> 一样，<code>select</code> 只执行一个 case，不会像 C 语言那样&quot;掉下去&quot;执行下一个 case。</p><h3 id="9-2-case-里的表达式在-select-开始时求值">9.2 case 里的表达式在 <code>select</code> 开始时求值</h3><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="comment">// 每个 case 的 channel 表达式在 select 开始时就确定了</span></span><br><span class="line"><span class="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> v := &lt;-getChannel():  <span class="comment">// getChannel() 在 select 开始时就调用了</span></span><br><span class="line">    fmt.Println(v)</span><br><span class="line"><span class="keyword">case</span> ch &lt;- getValue():     <span class="comment">// getValue() 在 select 开始时就调用了</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;sent&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-3-for-select-退出要用-return-或标签">9.3 for-select 退出要用 <code>return</code> 或标签</h3><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="comment">// 错误：break 只跳出 select，不跳出 for</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-quit:</span><br><span class="line">        <span class="keyword">break</span>  <span class="comment">// 这只跳出 select，循环继续！</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法一：用 return</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-quit:</span><br><span class="line">        <span class="keyword">return</span>  <span class="comment">// 跳出整个函数</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法二：用标签</span></span><br><span class="line">Loop:</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-quit:</span><br><span class="line">            <span class="keyword">break</span> Loop  <span class="comment">// 跳出标签对应的 for 循环</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="comment">// 代码继续执行...</span></span><br></pre></td></tr></table></figure><p>这是 Go 并发编程中一个常见的坑：在 <code>for-select</code> 中，<code>break</code> 默认只跳出 <code>select</code>，不跳出外层的 <code>for</code>。如果想跳出循环，要用 <code>return</code> 或带标签的 <code>break</code>。</p><hr><h2 id="10-实战综合示例">10. 实战综合示例</h2><h3 id="10-1-限时任务处理器">10.1 限时任务处理器</h3><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><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></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;math/rand&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="keyword">type</span> Job <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID   <span class="type">int</span></span><br><span class="line">    Data <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Result <span class="keyword">struct</span> &#123;</span><br><span class="line">    JobID <span class="type">int</span></span><br><span class="line">    Value <span class="type">string</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">processJob</span><span class="params">(job Job)</span></span> Result &#123;</span><br><span class="line">    <span class="comment">// 模拟处理耗时</span></span><br><span class="line">    delay := time.Duration(<span class="number">50</span>+rand.Intn(<span class="number">200</span>)) * time.Millisecond</span><br><span class="line">    time.Sleep(delay)</span><br><span class="line">    <span class="keyword">return</span> Result&#123;</span><br><span class="line">        JobID: job.ID,</span><br><span class="line">        Value: fmt.Sprintf(<span class="string">&quot;处理结果: %s (耗时 %v)&quot;</span>, job.Data, delay),</span><br><span class="line">    &#125;</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">    jobs := <span class="built_in">make</span>(<span class="keyword">chan</span> Job, <span class="number">10</span>)</span><br><span class="line">    results := <span class="built_in">make</span>(<span class="keyword">chan</span> Result, <span class="number">10</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 3 个 worker</span></span><br><span class="line">    <span class="keyword">for</span> w := <span class="number">1</span>; w &lt;= <span class="number">3</span>; w++ &#123;</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(workerID <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">for</span> job := <span class="keyword">range</span> jobs &#123;</span><br><span class="line">                fmt.Printf(<span class="string">&quot;Worker %d 处理任务 #%d\n&quot;</span>, workerID, job.ID)</span><br><span class="line">                results &lt;- processJob(job)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;(w)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 发送 8 个任务</span></span><br><span class="line">    totalJobs := <span class="number">8</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= totalJobs; i++ &#123;</span><br><span class="line">        jobs &lt;- Job&#123;ID: i, Data: fmt.Sprintf(<span class="string">&quot;任务数据_%d&quot;</span>, i)&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">close</span>(jobs)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集结果，最多等 3 秒</span></span><br><span class="line">    timeout := time.After(<span class="number">3</span> * time.Second)</span><br><span class="line">    collected := <span class="number">0</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> collected &lt; totalJobs &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> r := &lt;-results:</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  结果: %s\n&quot;</span>, r.Value)</span><br><span class="line">            collected++</span><br><span class="line">        <span class="keyword">case</span> &lt;-timeout:</span><br><span class="line">            fmt.Printf(<span class="string">&quot;超时！只收到 %d/%d 个结果\n&quot;</span>, collected, totalJobs)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;全部完成！收到 %d/%d 个结果\n&quot;</span>, collected, totalJobs)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-2-多路数据合并">10.2 多路数据合并</h3><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><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</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;time&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// merge 把多个 channel 合并成一个</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">merge</span><span class="params">(channels ...&lt;-<span class="keyword">chan</span> <span class="type">string</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">string</span> &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 为每个输入 channel 启动一个 goroutine</span></span><br><span class="line">    remaining := <span class="built_in">len</span>(channels)</span><br><span class="line">    done := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, ch := <span class="keyword">range</span> channels &#123;</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(c &lt;-<span class="keyword">chan</span> <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">for</span> v := <span class="keyword">range</span> c &#123;</span><br><span class="line">                out &lt;- v</span><br><span class="line">            &#125;</span><br><span class="line">            done &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">        &#125;(ch)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 等所有输入 channel 关闭后，关闭输出 channel</span></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">        <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; remaining; i++ &#123;</span><br><span class="line">            &lt;-done</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> out</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">dataSource</span><span class="params">(name <span class="type">string</span>, count <span class="type">int</span>, interval time.Duration)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">string</span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</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">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= count; i++ &#123;</span><br><span class="line">            time.Sleep(interval)</span><br><span class="line">            ch &lt;- fmt.Sprintf(<span class="string">&quot;[%s] 数据 %d&quot;</span>, name, i)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(ch)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> ch</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="comment">// 三个不同频率的数据源</span></span><br><span class="line">    src1 := dataSource(<span class="string">&quot;快&quot;</span>, <span class="number">5</span>, <span class="number">100</span>*time.Millisecond)</span><br><span class="line">    src2 := dataSource(<span class="string">&quot;中&quot;</span>, <span class="number">3</span>, <span class="number">250</span>*time.Millisecond)</span><br><span class="line">    src3 := dataSource(<span class="string">&quot;慢&quot;</span>, <span class="number">2</span>, <span class="number">400</span>*time.Millisecond)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 合并成一个 channel</span></span><br><span class="line">    merged := merge(src1, src2, src3)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 从合并后的 channel 接收所有数据</span></span><br><span class="line">    <span class="keyword">for</span> msg := <span class="keyword">range</span> merged &#123;</span><br><span class="line">        fmt.Println(msg)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;所有数据源已完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个 <code>merge</code> 函数把多个 channel 合并成一个，是 Go 并发编程中很经典的工具。</p><hr><h2 id="11-常见坑总结">11. 常见坑总结</h2><h3 id="11-1-循环中的-time-After-会泄漏">11.1 循环中的 <code>time.After</code> 会泄漏</h3><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="comment">// 有问题：每次循环都创建一个新的 timer，旧的不会被回收</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        handle(msg)</span><br><span class="line">    <span class="keyword">case</span> &lt;-time.After(<span class="number">5</span> * time.Second):  <span class="comment">// 每次循环创建新的！</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;超时&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每次循环都会调用 <code>time.After</code>，创建一个新的定时器。如果 <code>ch</code> 频繁有数据，旧的定时器还没触发就被丢弃了，但它们仍然在内存中直到触发时间到。在高频循环中会造成内存泄漏。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 正确：用 time.NewTimer 并手动重置</span></span><br><span class="line">timer := time.NewTimer(<span class="number">5</span> * time.Second)</span><br><span class="line"><span class="keyword">defer</span> timer.Stop()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        handle(msg)</span><br><span class="line">        <span class="comment">// 重置定时器</span></span><br><span class="line">        <span class="keyword">if</span> !timer.Stop() &#123;</span><br><span class="line">            &lt;-timer.C</span><br><span class="line">        &#125;</span><br><span class="line">        timer.Reset(<span class="number">5</span> * time.Second)</span><br><span class="line">    <span class="keyword">case</span> &lt;-timer.C:</span><br><span class="line">        fmt.Println(<span class="string">&quot;超时&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>简单规则</strong>：<code>time.After</code> 适合一次性的超时。循环中反复超时，用 <code>time.NewTimer</code>。</p><h3 id="11-2-for-select-中-break-的陷阱">11.2 <code>for-select</code> 中 <code>break</code> 的陷阱</h3><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="comment">// 错误：break 只跳出 select</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-quit:</span><br><span class="line">        <span class="keyword">break</span>  <span class="comment">// 不会退出 for 循环！</span></span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        fmt.Println(msg)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：用 return 或标签</span></span><br></pre></td></tr></table></figure><p>前面第 9.3 节详细讲过，再强调一遍——这是非常高频的 bug。</p><h3 id="11-3-忘记-default-导致意外阻塞">11.3 忘记 <code>default</code> 导致意外阻塞</h3><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="comment">// 本意是&quot;尝试发送，不行就算了&quot;</span></span><br><span class="line"><span class="comment">// 但忘了 default，如果 ch 满了就会永久阻塞</span></span><br><span class="line"><span class="keyword">select</span> &#123;</span><br><span class="line"><span class="keyword">case</span> ch &lt;- data:</span><br><span class="line">    fmt.Println(<span class="string">&quot;发送成功&quot;</span>)</span><br><span class="line"><span class="comment">// 忘了 default!</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要非阻塞操作时一定要加 <code>default</code>。</p><h3 id="11-4-default-导致-CPU-空转">11.4 <code>default</code> 导致 CPU 空转</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 有 default 的空循环会疯狂消耗 CPU</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        handle(msg)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="comment">// 什么都不做，CPU 一直在这转圈</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 加个 sleep 缓解</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> msg := &lt;-ch:</span><br><span class="line">        handle(msg)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        time.Sleep(<span class="number">10</span> * time.Millisecond)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更好的做法：不用 default，让 select 阻塞等待</span></span><br><span class="line"><span class="keyword">for</span> msg := <span class="keyword">range</span> ch &#123;</span><br><span class="line">    handle(msg)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-5-Ticker-不停止会泄漏">11.5 Ticker 不停止会泄漏</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 有问题：ticker 永远不会停止</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">f</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ticker := time.NewTicker(time.Second)</span><br><span class="line">    <span class="keyword">for</span> <span class="keyword">range</span> ticker.C &#123;</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        <span class="keyword">if</span> done &#123;</span><br><span class="line">            <span class="keyword">return</span>  <span class="comment">// 函数返回了，但 ticker 还在运行</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：defer 停止</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">f</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ticker := time.NewTicker(time.Second)</span><br><span class="line">    <span class="keyword">defer</span> ticker.Stop()  <span class="comment">// 函数返回时停止 ticker</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="12-本课练习">12. 本课练习</h2><h3 id="练习-1：超时查询">练习 1：超时查询</h3><p>要求：</p><ul><li>写一个函数模拟数据库查询，随机耗时 1~5 秒</li><li>主 goroutine 调用这个查询，但最多等 3 秒</li><li>如果在 3 秒内返回，打印结果</li><li>如果超时，打印&quot;查询超时&quot;</li></ul><hr><h3 id="练习-2：优先级-channel">练习 2：优先级 channel</h3><p>要求：</p><ul><li>创建两个 channel：<code>highPriority</code> 和 <code>lowPriority</code></li><li>两个 goroutine 分别向两个 channel 发送数据</li><li>用 <code>select</code> 接收数据，当两个都有数据时，优先处理 <code>highPriority</code></li></ul><p>提示：可以用嵌套 select 或先尝试非阻塞接收高优先级。</p><hr><h3 id="练习-3：倒计时器">练习 3：倒计时器</h3><p>要求：</p><ul><li>实现一个 10 秒倒计时</li><li>每秒打印剩余秒数</li><li>期间用户可以输入 “stop” 来提前停止</li><li>倒计时结束打印 “发射！”，提前停止打印 “已取消”</li></ul><p>提示：用一个 goroutine 读用户输入，通过 channel 传递给 select。</p><hr><h3 id="练习-4：限速器">练习 4：限速器</h3><p>要求：</p><ul><li>写一个函数，接收请求但限制为每秒最多 3 个</li><li>用 <code>time.Ticker</code> 实现限速</li><li>超出速率的请求等待，超过 5 秒等不到就丢弃</li></ul><hr><h3 id="练习-5：多源合并排序">练习 5：多源合并排序</h3><p>要求：</p><ul><li>三个 goroutine 各自生成一组有序数字，发送到各自的 channel</li><li>写一个函数，用 <code>select</code> 从三个 channel 中接收，合并成一个有序输出</li></ul><p>提示：这是归并排序中&quot;合并&quot;步骤的并发版本。</p><hr><h2 id="13-自测题">13. 自测题</h2><h3 id="13-1-概念题">13.1 概念题</h3><ol><li><code>select</code> 和 <code>switch</code> 最大的区别是什么？</li><li>多个 case 同时就绪时，<code>select</code> 怎么选择？为什么这样设计？</li><li><code>select</code> 中 <code>default</code> 的作用是什么？什么时候该用，什么时候不该用？</li><li><code>time.After</code> 返回的是什么？它的原理是什么？</li><li><code>for-select</code> 中 <code>break</code> 跳出的是什么？怎么跳出外层循环？</li><li><code>time.NewTicker</code> 和 <code>time.Tick</code> 有什么区别？为什么推荐用 <code>NewTicker</code>？</li><li>空 <code>select &#123;&#125;</code> 会怎样？什么时候用？</li><li>在循环中使用 <code>time.After</code> 有什么问题？</li></ol><h3 id="13-2-代码阅读题">13.2 代码阅读题</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><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;fmt&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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">1</span>)</span><br><span class="line"></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">        <span class="keyword">for</span> i := <span class="number">1</span>; ; i++ &#123;</span><br><span class="line">            ch &lt;- i</span><br><span class="line">            time.Sleep(<span class="number">300</span> * time.Millisecond)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> v := &lt;-ch:</span><br><span class="line">            fmt.Println(<span class="string">&quot;收到:&quot;</span>, v)</span><br><span class="line">        <span class="keyword">case</span> &lt;-time.After(<span class="number">1</span> * time.Second):</span><br><span class="line">            fmt.Println(<span class="string">&quot;超时退出&quot;</span>)</span><br><span class="line">            <span class="keyword">return</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><details><summary>点击查看答案</summary><p>程序会持续输出：</p><figure class="highlight makefile"><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="section">收到: 1</span></span><br><span class="line"><span class="section">收到: 2</span></span><br><span class="line"><span class="section">收到: 3</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p><strong>永远不会超时退出</strong>。</p><p>解释：</p><ol><li>goroutine 每 300ms 发送一个数字</li><li><code>select</code> 每次循环时创建一个新的 <code>time.After(1 * time.Second)</code></li><li>因为每 300ms 就能从 <code>ch</code> 收到数据，<code>time.After</code> 的 1 秒还没到就被新一轮循环替换了</li><li>每次循环都是新的 <code>time.After</code>，所以超时永远不会触发</li></ol><p>如果想要&quot;1 秒内没有任何数据就超时&quot;，应该把 <code>time.After</code> 或 <code>time.NewTimer</code> 放在循环<strong>外面</strong>，或者在收到数据后重置定时器。</p></details><hr><h2 id="14-本课总结">14. 本课总结</h2><p>这一课你学到了 <code>select</code>——Go 并发编程中的多路复用器。</p><table><thead><tr><th>场景</th><th>做法</th></tr></thead><tbody><tr><td>同时监听多个 channel</td><td><code>select</code> + 多个 <code>case</code></td></tr><tr><td>超时控制</td><td><code>case &lt;-time.After(d)</code></td></tr><tr><td>定时任务</td><td><code>time.NewTicker</code> + <code>select</code></td></tr><tr><td>非阻塞操作</td><td><code>select</code> + <code>default</code></td></tr><tr><td>退出信号</td><td><code>case &lt;-quit</code>，quit 用 <code>close</code> 触发</td></tr><tr><td>多服务竞速</td><td>多个 <code>case</code> 接收，先到先用</td></tr><tr><td>持续监听</td><td><code>for &#123; select &#123; ... &#125; &#125;</code></td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong><code>select</code> 监听多个 channel，谁先就绪执行谁——多个同时就绪则随机选</strong></li><li><strong><code>time.After</code> 配合 <code>select</code> 实现超时——这是 Go 中最常见的超时方式</strong></li><li><strong><code>for-select</code> 中 <code>break</code> 只跳出 <code>select</code>——要退出循环用 <code>return</code> 或标签</strong></li></ol><hr><h2 id="15-下一课预告">15. 下一课预告</h2><p>到目前为止你学了 goroutine、channel、select，已经能写不少并发程序了。但有一类问题还没解决：<strong>多个 goroutine 访问同一个共享资源怎么办？</strong></p><p>比如多个 goroutine 同时往一个 map 里写数据，程序直接崩溃。多个 goroutine 同时修改一个计数器，结果不对。这些都是<strong>并发安全</strong>问题。</p><p>下一课：<strong>同步原语</strong></p><p>会重点讲：</p><ul><li><code>sync.Mutex</code>——互斥锁，保护共享资源</li><li><code>sync.RWMutex</code>——读写锁，读多写少的场景</li><li><code>sync.Once</code>——只执行一次</li><li>什么是数据竞争，怎么用 <code>go run -race</code> 检测</li><li>什么时候用锁，什么时候用 channel</li></ul><p>学完下一课，你就能写出安全可靠的并发程序了。</p>]]></content>
    
    
    <summary type="html">Go语言系列27</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 26 课：channel 入门</title>
    <link href="https://yjyrichard.github.io/posts/8e0fcd91.html"/>
    <id>https://yjyrichard.github.io/posts/8e0fcd91.html</id>
    <published>2026-03-22T15:28:11.721Z</published>
    <updated>2026-03-22T15:40:40.009Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 26 课：channel 入门</h1><blockquote><p>学习定位：这是整套 Go 教程的第 26 课，也是阶段五（并发与工程阶段）的第二课。<br>前置要求：已经完成第 25 课，理解 goroutine 的创建和 WaitGroup 的使用。<br>本课目标：理解 channel 的概念和工作机制，掌握无缓冲 channel 和有缓冲 channel 的区别，能用 channel 在 goroutine 之间安全地传递数据，理解 channel 的阻塞行为和关闭机制。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>上一课你学会了用 goroutine 并行执行任务。但有一个问题没解决：<strong>goroutine 之间怎么通信？</strong></p><p>上一课我们用&quot;预分配切片 + 按索引写入&quot;来收集结果，这在简单场景下可以。但如果需求变复杂一点——</p><ul><li>一个 goroutine 产生数据，另一个消费数据</li><li>多个 goroutine 协作完成一个流水线任务</li><li>一个 goroutine 通知另一个&quot;可以开始了&quot;</li></ul><p>这些都需要 goroutine 之间能<strong>安全地传递数据</strong>。Go 给出的答案就是 <strong>channel</strong>。</p><p>Go 有一句著名的并发哲学：</p><blockquote><p><strong>“不要通过共享内存来通信，而要通过通信来共享内存。”</strong></p></blockquote><p>channel 就是这句话的具体实现。</p><p>你需要搞明白以下问题：</p><ul><li>channel 是什么</li><li>怎么创建 channel，怎么发送和接收数据</li><li>无缓冲 channel 和有缓冲 channel 有什么区别</li><li>channel 什么时候会阻塞</li><li>怎么关闭 channel，关闭后会怎样</li><li>怎么用 <code>range</code> 遍历 channel</li><li>什么是单向 channel</li></ul><p>学完这一课，你就掌握了 Go 并发编程最核心的通信工具。</p><hr><h2 id="2-channel-是什么">2. channel 是什么</h2><h3 id="2-1-一句话定义">2.1 一句话定义</h3><p><strong>channel 是 goroutine 之间传递数据的管道。</strong></p><p>你可以把 channel 想象成一根管子：一头塞东西进去（发送），另一头把东西拿出来（接收）。</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">goroutine <span class="selector-tag">A</span>  ──发送──▶  <span class="selector-attr">[ channel ]</span>  ──接收──▶  goroutine <span class="selector-tag">B</span></span><br></pre></td></tr></table></figure><h3 id="2-2-channel-的类型">2.2 channel 的类型</h3><p>channel 是有类型的。一个 <code>chan int</code> 只能传 <code>int</code>，一个 <code>chan string</code> 只能传 <code>string</code>：</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> ch1 <span class="keyword">chan</span> <span class="type">int</span>      <span class="comment">// 传 int 的 channel</span></span><br><span class="line"><span class="keyword">var</span> ch2 <span class="keyword">chan</span> <span class="type">string</span>   <span class="comment">// 传 string 的 channel</span></span><br><span class="line"><span class="keyword">var</span> ch3 <span class="keyword">chan</span> <span class="type">bool</span>     <span class="comment">// 传 bool 的 channel</span></span><br><span class="line"><span class="keyword">var</span> ch4 <span class="keyword">chan</span> []<span class="type">byte</span>   <span class="comment">// 传 []byte 的 channel</span></span><br></pre></td></tr></table></figure><h3 id="2-3-channel-的零值">2.3 channel 的零值</h3><p>channel 的零值是 <code>nil</code>。对 <code>nil</code> channel 发送或接收会<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> ch <span class="keyword">chan</span> <span class="type">int</span>  <span class="comment">// nil</span></span><br><span class="line">ch &lt;- <span class="number">1</span>         <span class="comment">// 永久阻塞</span></span><br><span class="line">&lt;-ch            <span class="comment">// 永久阻塞</span></span><br></pre></td></tr></table></figure><p>所以 channel 必须用 <code>make</code> 创建后才能使用。</p><hr><h2 id="3-创建-channel">3. 创建 channel</h2><h3 id="3-1-无缓冲-channel">3.1 无缓冲 channel</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)  <span class="comment">// 创建一个无缓冲的 int channel</span></span><br></pre></td></tr></table></figure><p>无缓冲 channel 没有任何存储空间。发送方发送数据后必须等接收方来取，接收方想取数据也必须等发送方来送。<strong>两方必须同时到位，数据才能传递</strong>。</p><p>就像面对面交接物品：你必须等对方伸手才能把东西给出去。</p><h3 id="3-2-有缓冲-channel">3.2 有缓冲 channel</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">5</span>)  <span class="comment">// 创建一个容量为 5 的有缓冲 channel</span></span><br></pre></td></tr></table></figure><p>有缓冲 channel 有一个内部队列，可以暂存数据。发送方可以往里塞数据，只要队列没满就不会阻塞。接收方从队列中取数据，只要队列不空就不会阻塞。</p><p>就像一个快递柜：寄件人放进去就走，取件人有空再来取。柜子放满了，寄件人才需要等。</p><hr><h2 id="4-发送和接收">4. 发送和接收</h2><h3 id="4-1-操作符">4.1 操作符 <code>&lt;-</code></h3><p>channel 用 <code>&lt;-</code> 操作符发送和接收数据：</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">ch &lt;- value   <span class="comment">// 发送：把 value 发送到 ch</span></span><br><span class="line">v := &lt;-ch     <span class="comment">// 接收：从 ch 接收一个值，赋给 v</span></span><br><span class="line">&lt;-ch          <span class="comment">// 接收：从 ch 接收一个值，丢弃</span></span><br></pre></td></tr></table></figure><p><strong>箭头方向就是数据流动方向</strong>：<code>ch &lt;- value</code> 是数据流入 channel，<code>&lt;-ch</code> 是数据从 channel 流出。</p><h3 id="4-2-第一个-channel-示例">4.2 第一个 channel 示例</h3><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 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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)  <span class="comment">// 创建无缓冲 channel</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在新的 goroutine 中发送数据</span></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">        ch &lt;- <span class="string">&quot;你好，channel！&quot;</span>  <span class="comment">// 发送</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    msg := &lt;-ch  <span class="comment">// 接收（会阻塞，直到有数据可收）</span></span><br><span class="line">    fmt.Println(msg)  <span class="comment">// 你好，channel！</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行流程：</p><ol><li>创建 channel</li><li>启动一个 goroutine，它尝试往 channel 发送数据</li><li>主 goroutine 执行 <code>&lt;-ch</code>，阻塞等待数据</li><li>新 goroutine 发送 <code>&quot;你好，channel！&quot;</code></li><li>主 goroutine 收到数据，解除阻塞，打印结果</li></ol><p><strong>注意</strong>：这里不需要 <code>WaitGroup</code>。<code>&lt;-ch</code> 本身就是一种等待机制——主 goroutine 会一直阻塞，直到收到数据。channel 天然地起到了<strong>同步</strong>的作用。</p><h3 id="4-3-用-channel-替代-WaitGroup">4.3 用 channel 替代 WaitGroup</h3><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></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">worker</span><span class="params">(id <span class="type">int</span>, done <span class="keyword">chan</span> <span class="type">bool</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 开始\n&quot;</span>, id)</span><br><span class="line">    <span class="comment">// 做一些工作...</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 完成\n&quot;</span>, id)</span><br><span class="line">    done &lt;- <span class="literal">true</span>  <span class="comment">// 通知主 goroutine：我完成了</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">    done := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">bool</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> worker(<span class="number">1</span>, done)</span><br><span class="line"></span><br><span class="line">    &lt;-done  <span class="comment">// 等待 worker 完成</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;主 goroutine 结束&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>done &lt;- true</code> 就是 worker 在说&quot;我干完了&quot;，<code>&lt;-done</code> 就是主 goroutine 在等这句话。</p><hr><h2 id="5-无缓冲-channel-的阻塞行为">5. 无缓冲 channel 的阻塞行为</h2><h3 id="5-1-发送阻塞，直到有人接收">5.1 发送阻塞，直到有人接收</h3><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)  <span class="comment">// 无缓冲</span></span><br><span class="line"></span><br><span class="line">    ch &lt;- <span class="number">42</span>  <span class="comment">// 阻塞！没有其他 goroutine 来接收</span></span><br><span class="line">    <span class="comment">// 永远执行不到这里</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(&lt;-ch)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行这段代码，你会得到一个**死锁（deadlock）**错误：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">fatal</span> <span class="literal">error</span>: all goroutines are asleep - deadlock!</span><br></pre></td></tr></table></figure><p>因为发送操作 <code>ch &lt;- 42</code> 需要另一个 goroutine 来接收，但只有一个主 goroutine，没人来接收，所以永远阻塞。Go 运行时检测到所有 goroutine 都在等待，就报死锁。</p><h3 id="5-2-接收阻塞，直到有人发送">5.2 接收阻塞，直到有人发送</h3><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="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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"></span><br><span class="line">    v := &lt;-ch  <span class="comment">// 阻塞！没有其他 goroutine 来发送</span></span><br><span class="line">    fmt.Println(v)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样是死锁。接收操作需要有人发送数据。</p><h3 id="5-3-无缓冲-channel-是同步的">5.3 无缓冲 channel 是同步的</h3><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></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;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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">string</span>)</span><br><span class="line"></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(<span class="string">&quot;goroutine: 准备发送...&quot;</span>)</span><br><span class="line">        time.Sleep(<span class="number">2</span> * time.Second)  <span class="comment">// 模拟耗时操作</span></span><br><span class="line">        ch &lt;- <span class="string">&quot;数据来了&quot;</span></span><br><span class="line">        fmt.Println(<span class="string">&quot;goroutine: 发送完成&quot;</span>)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;main: 等待接收...&quot;</span>)</span><br><span class="line">    msg := &lt;-ch  <span class="comment">// 阻塞 2 秒，直到 goroutine 发送</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;main: 收到:&quot;</span>, msg)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight avrasm"><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="symbol">main:</span> 等待接收...</span><br><span class="line"><span class="symbol">goroutine:</span> 准备发送...</span><br><span class="line">（等待 <span class="number">2</span> 秒）</span><br><span class="line"><span class="symbol">goroutine:</span> 发送完成</span><br><span class="line"><span class="symbol">main:</span> 收到: 数据来了</span><br></pre></td></tr></table></figure><p>无缓冲 channel 的核心特性：<strong>发送和接收必须同时就绪</strong>。这让它成为 goroutine 之间最简单的同步工具。</p><hr><h2 id="6-有缓冲-channel">6. 有缓冲 channel</h2><h3 id="6-1-基本用法">6.1 基本用法</h3><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">3</span>)  <span class="comment">// 容量为 3</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 可以连续发送 3 个值，不会阻塞（因为缓冲区没满）</span></span><br><span class="line">    ch &lt;- <span class="number">1</span></span><br><span class="line">    ch &lt;- <span class="number">2</span></span><br><span class="line">    ch &lt;- <span class="number">3</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// ch &lt;- 4  // 这里会阻塞！缓冲区已满</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 1（先进先出）</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 2</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 3</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有缓冲 channel 像一个队列：<strong>先进先出（FIFO）</strong>。</p><h3 id="6-2-阻塞规则">6.2 阻塞规则</h3><figure class="highlight"><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><br><span class="line">接收：缓冲区空了才阻塞</span><br></pre></td></tr></table></figure><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">ch &lt;- <span class="number">1</span>   <span class="comment">// 不阻塞（缓冲区: [1]，容量 2）</span></span><br><span class="line">ch &lt;- <span class="number">2</span>   <span class="comment">// 不阻塞（缓冲区: [1, 2]，容量 2）</span></span><br><span class="line"><span class="comment">// ch &lt;- 3  // 阻塞！缓冲区满了</span></span><br><span class="line"></span><br><span class="line">&lt;-ch      <span class="comment">// 不阻塞，取出 1（缓冲区: [2]）</span></span><br><span class="line">ch &lt;- <span class="number">3</span>   <span class="comment">// 不阻塞了（缓冲区: [2, 3]）</span></span><br></pre></td></tr></table></figure><h3 id="6-3-len-和-cap">6.3 <code>len</code> 和 <code>cap</code></h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">5</span>)</span><br><span class="line"></span><br><span class="line">ch &lt;- <span class="number">10</span></span><br><span class="line">ch &lt;- <span class="number">20</span></span><br><span class="line">ch &lt;- <span class="number">30</span></span><br><span class="line"></span><br><span class="line">fmt.Println(<span class="built_in">len</span>(ch))  <span class="comment">// 3（当前缓冲区中有 3 个元素）</span></span><br><span class="line">fmt.Println(<span class="built_in">cap</span>(ch))  <span class="comment">// 5（缓冲区总容量）</span></span><br></pre></td></tr></table></figure><h3 id="6-4-无缓冲-vs-有缓冲">6.4 无缓冲 vs 有缓冲</h3><table><thead><tr><th>特性</th><th>无缓冲 <code>make(chan T)</code></th><th>有缓冲 <code>make(chan T, n)</code></th></tr></thead><tbody><tr><td>容量</td><td>0</td><td>n</td></tr><tr><td>发送阻塞</td><td>直到有人接收</td><td>直到缓冲区满</td></tr><tr><td>接收阻塞</td><td>直到有人发送</td><td>直到缓冲区空</td></tr><tr><td>同步性</td><td>强同步（握手）</td><td>弱同步（排队）</td></tr><tr><td>类比</td><td>面对面交接</td><td>快递柜</td></tr></tbody></table><p><strong>怎么选</strong>：</p><ul><li>需要严格同步（确保一方完成后另一方才继续）→ 无缓冲</li><li>允许发送方先跑一段，不必等接收方 → 有缓冲</li><li>不确定用哪个 → 先用无缓冲，需要优化时再加缓冲</li></ul><hr><h2 id="7-关闭-channel">7. 关闭 channel</h2><h3 id="7-1-为什么要关闭">7.1 为什么要关闭</h3><p>当发送方不再发送数据时，需要告诉接收方&quot;没有更多数据了&quot;。这就是关闭 channel 的作用。</p><h3 id="7-2-close-函数">7.2 <code>close</code> 函数</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">ch &lt;- <span class="number">1</span></span><br><span class="line">ch &lt;- <span class="number">2</span></span><br><span class="line">ch &lt;- <span class="number">3</span></span><br><span class="line"><span class="built_in">close</span>(ch)  <span class="comment">// 关闭 channel</span></span><br></pre></td></tr></table></figure><h3 id="7-3-关闭后的行为">7.3 关闭后的行为</h3><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">    ch &lt;- <span class="number">10</span></span><br><span class="line">    ch &lt;- <span class="number">20</span></span><br><span class="line">    <span class="built_in">close</span>(ch)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 关闭后仍然可以接收缓冲区中的数据</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 10</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 20</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 缓冲区空了，继续接收会得到零值</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 0（int 的零值）</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 0（不会阻塞，也不会 panic）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关闭后的规则</strong>：</p><table><thead><tr><th>操作</th><th>结果</th></tr></thead><tbody><tr><td>接收（缓冲区还有数据）</td><td>正常返回数据</td></tr><tr><td>接收（缓冲区空了）</td><td>返回零值，不阻塞</td></tr><tr><td>发送</td><td><strong>panic!</strong></td></tr><tr><td>再次关闭</td><td><strong>panic!</strong></td></tr></tbody></table><h3 id="7-4-判断-channel-是否已关闭">7.4 判断 channel 是否已关闭</h3><p>接收操作可以返回两个值：</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">v, ok := &lt;-ch</span><br></pre></td></tr></table></figure><ul><li><code>ok == true</code>：收到了正常的数据</li><li><code>ok == false</code>：channel 已关闭且缓冲区为空</li></ul><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">2</span>)</span><br><span class="line">    ch &lt;- <span class="number">1</span></span><br><span class="line">    ch &lt;- <span class="number">2</span></span><br><span class="line">    <span class="built_in">close</span>(ch)</span><br><span class="line"></span><br><span class="line">    v, ok := &lt;-ch</span><br><span class="line">    fmt.Println(v, ok)  <span class="comment">// 1 true</span></span><br><span class="line"></span><br><span class="line">    v, ok = &lt;-ch</span><br><span class="line">    fmt.Println(v, ok)  <span class="comment">// 2 true</span></span><br><span class="line"></span><br><span class="line">    v, ok = &lt;-ch</span><br><span class="line">    fmt.Println(v, ok)  <span class="comment">// 0 false（channel 已关闭且为空）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-5-谁来关闭-channel">7.5 谁来关闭 channel</h3><p><strong>原则：只有发送方关闭 channel，接收方不要关闭。</strong></p><p>因为发送方知道自己什么时候不再发送了。如果接收方关闭了 channel，发送方继续发送就会 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="comment">// 正确：发送方关闭</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">producer</span><span class="params">(ch <span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">        ch &lt;- i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">close</span>(ch)  <span class="comment">// 发送完了，关闭</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 错误：接收方关闭</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">consumer</span><span class="params">(ch <span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// close(ch)  // 不要在这里关闭！</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="8-range-遍历-channel">8. <code>range</code> 遍历 channel</h2><h3 id="8-1-基本用法">8.1 基本用法</h3><p><code>for range</code> 可以遍历 channel，它会一直接收数据，<strong>直到 channel 被关闭</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><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 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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"></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">        <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">            ch &lt;- i</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(ch)  <span class="comment">// 发送完毕，关闭 channel</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// range 会一直接收，直到 ch 被关闭</span></span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;接收完毕&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight"><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">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">接收完毕</span><br></pre></td></tr></table></figure><h3 id="8-2-如果不关闭-channel">8.2 如果不关闭 channel</h3><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></pre></td><td class="code"><pre><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">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">        ch &lt;- i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 忘了 close(ch)</span></span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">    fmt.Println(v)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 打印完 1~5 后，range 会一直阻塞等待更多数据</span></span><br><span class="line"><span class="comment">// 最终报 deadlock 错误</span></span><br></pre></td></tr></table></figure><p><strong><code>for range</code> 遍历 channel 时，必须有人关闭 channel</strong>，否则 range 永远不会结束。</p><h3 id="8-3-多个发送者的情况">8.3 多个发送者的情况</h3><p>如果有多个 goroutine 往同一个 channel 发送数据，什么时候关闭？</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="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;sync&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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动 3 个发送者</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="keyword">for</span> j := <span class="number">1</span>; j &lt;= <span class="number">3</span>; j++ &#123;</span><br><span class="line">                ch &lt;- id*<span class="number">10</span> + j</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 用一个单独的 goroutine 等待所有发送者完成后关闭 channel</span></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">        wg.Wait()</span><br><span class="line">        <span class="built_in">close</span>(ch)</span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 主 goroutine 接收</span></span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>技巧</strong>：用 <code>WaitGroup</code> 等待所有发送者完成，然后在一个单独的 goroutine 中关闭 channel。这样 <code>range</code> 就能正常结束了。</p><hr><h2 id="9-单向-channel">9. 单向 channel</h2><h3 id="9-1-为什么需要单向-channel">9.1 为什么需要单向 channel</h3><p>有时候你希望一个函数只能往 channel 发送数据，另一个函数只能从 channel 接收数据。这样可以防止误操作。</p><h3 id="9-2-语法">9.2 语法</h3><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">chan</span>&lt;- <span class="type">int</span>  <span class="comment">// 只能发送的 channel（send-only）</span></span><br><span class="line">&lt;-<span class="keyword">chan</span> <span class="type">int</span>  <span class="comment">// 只能接收的 channel（receive-only）</span></span><br></pre></td></tr></table></figure><p><strong>记忆方法</strong>：箭头指向 <code>chan</code> 就是发送，箭头从 <code>chan</code> 出来就是接收。</p><h3 id="9-3-用在函数参数中">9.3 用在函数参数中</h3><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 class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// producer 只能往 ch 发送数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">producer</span><span class="params">(ch <span class="keyword">chan</span>&lt;- <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">        ch &lt;- i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">close</span>(ch)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// consumer 只能从 ch 接收数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">consumer</span><span class="params">(ch &lt;-<span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;收到: %d\n&quot;</span>, v)</span><br><span class="line">    &#125;</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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)  <span class="comment">// 双向 channel</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> producer(ch)  <span class="comment">// 自动转换为 chan&lt;- int</span></span><br><span class="line">    consumer(ch)     <span class="comment">// 自动转换为 &lt;-chan int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>双向 channel 可以<strong>隐式转换</strong>为单向 channel，但单向不能转回双向。这是一种类型安全机制：</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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> sendOnly <span class="keyword">chan</span>&lt;- <span class="type">int</span> = ch   <span class="comment">// 可以：双向 → 只发送</span></span><br><span class="line"><span class="keyword">var</span> recvOnly &lt;-<span class="keyword">chan</span> <span class="type">int</span> = ch   <span class="comment">// 可以：双向 → 只接收</span></span><br><span class="line"><span class="comment">// var ch2 chan int = sendOnly // 错误：只发送 → 双向，不允许</span></span><br></pre></td></tr></table></figure><h3 id="9-4-单向-channel-的好处">9.4 单向 channel 的好处</h3><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="comment">// 如果 producer 不小心写了 &lt;-ch，编译器直接报错</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">producer</span><span class="params">(ch <span class="keyword">chan</span>&lt;- <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="comment">// v := &lt;-ch  // 编译错误：cannot receive from send-only channel</span></span><br><span class="line">    ch &lt;- <span class="number">42</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译器帮你检查，防止在不该接收的地方接收、不该发送的地方发送。</p><hr><h2 id="10-实战示例">10. 实战示例</h2><h3 id="10-1-生产者-消费者模式">10.1 生产者-消费者模式</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><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></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;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">producer</span><span class="params">(ch <span class="keyword">chan</span>&lt;- <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">10</span>; i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;生产: %d\n&quot;</span>, i)</span><br><span class="line">        ch &lt;- i</span><br><span class="line">        time.Sleep(<span class="number">100</span> * time.Millisecond)  <span class="comment">// 模拟生产耗时</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">close</span>(ch)</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">consumer</span><span class="params">(ch &lt;-<span class="keyword">chan</span> <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  消费: %d\n&quot;</span>, v)</span><br><span class="line">        time.Sleep(<span class="number">200</span> * time.Millisecond)  <span class="comment">// 消费比生产慢</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println(<span class="string">&quot;消费者: 没有更多数据了&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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">3</span>)  <span class="comment">// 缓冲区为 3，允许生产者先跑一段</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> producer(ch)</span><br><span class="line">    consumer(ch)  <span class="comment">// 在主 goroutine 中消费</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight nestedtext"><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="attribute">生产</span><span class="punctuation">:</span> <span class="string">1</span></span><br><span class="line">  <span class="attribute">消费</span><span class="punctuation">:</span> <span class="string">1</span></span><br><span class="line"><span class="attribute">生产</span><span class="punctuation">:</span> <span class="string">2</span></span><br><span class="line"><span class="attribute">生产</span><span class="punctuation">:</span> <span class="string">3</span></span><br><span class="line"><span class="attribute">生产</span><span class="punctuation">:</span> <span class="string">4</span></span><br><span class="line">  <span class="attribute">消费</span><span class="punctuation">:</span> <span class="string">2</span></span><br><span class="line"><span class="attribute">生产</span><span class="punctuation">:</span> <span class="string">5</span></span><br><span class="line">  <span class="attribute">消费</span><span class="punctuation">:</span> <span class="string">3</span></span><br><span class="line"><span class="attribute">生产</span><span class="punctuation">:</span> <span class="string">6</span></span><br><span class="line">  <span class="attribute">消费</span><span class="punctuation">:</span> <span class="string">4</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>观察：生产者比消费者快，但缓冲区只有 3，所以生产者会被限速。这就是 channel 的<strong>背压</strong>（backpressure）机制——消费者处理不过来时，生产者会自动慢下来。</p><h3 id="10-2-用-channel-收集-goroutine-的结果">10.2 用 channel 收集 goroutine 的结果</h3><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></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;math/rand&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="keyword">type</span> Result <span class="keyword">struct</span> &#123;</span><br><span class="line">    WorkerID <span class="type">int</span></span><br><span class="line">    Value    <span class="type">int</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">worker</span><span class="params">(id <span class="type">int</span>, results <span class="keyword">chan</span>&lt;- Result)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 模拟耗时计算</span></span><br><span class="line">    time.Sleep(time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">400</span>)) * time.Millisecond)</span><br><span class="line">    results &lt;- Result&#123;WorkerID: id, Value: rand.Intn(<span class="number">100</span>)&#125;</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">    numWorkers := <span class="number">5</span></span><br><span class="line">    results := <span class="built_in">make</span>(<span class="keyword">chan</span> Result, numWorkers)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动多个 worker</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= numWorkers; i++ &#123;</span><br><span class="line">        <span class="keyword">go</span> worker(i, results)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 收集所有结果</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        r := &lt;-results</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Worker %d 返回: %d\n&quot;</span>, r.WorkerID, r.Value)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;所有结果已收集&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这比上一课用&quot;预分配切片 + 按索引写入&quot;更优雅：</p><ul><li>结果按<strong>完成顺序</strong>返回，先完成的先输出</li><li>不需要预先知道有多少结果</li><li>channel 保证了并发安全</li></ul><h3 id="10-3-流水线（Pipeline）">10.3 流水线（Pipeline）</h3><p>channel 可以把多个处理步骤串起来，形成数据处理流水线：</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></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="comment">// 第一步：生成数字</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">generate</span><span class="params">(nums ...<span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">int</span> &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</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">        <span class="keyword">for</span> _, n := <span class="keyword">range</span> nums &#123;</span><br><span class="line">            out &lt;- n</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二步：平方</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">square</span><span class="params">(in &lt;-<span class="keyword">chan</span> <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">int</span> &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</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">        <span class="keyword">for</span> n := <span class="keyword">range</span> in &#123;</span><br><span class="line">            out &lt;- n * n</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第三步：过滤（只保留大于 10 的数）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">filter</span><span class="params">(in &lt;-<span class="keyword">chan</span> <span class="type">int</span>, threshold <span class="type">int</span>)</span></span> &lt;-<span class="keyword">chan</span> <span class="type">int</span> &#123;</span><br><span class="line">    out := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</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">        <span class="keyword">for</span> n := <span class="keyword">range</span> in &#123;</span><br><span class="line">            <span class="keyword">if</span> n &gt; threshold &#123;</span><br><span class="line">                out &lt;- n</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">close</span>(out)</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">return</span> out</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="comment">// 构建流水线：生成 → 平方 → 过滤</span></span><br><span class="line">    nums := generate(<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>, <span class="number">7</span>, <span class="number">8</span>, <span class="number">9</span>, <span class="number">10</span>)</span><br><span class="line">    squared := square(nums)</span><br><span class="line">    result := filter(squared, <span class="number">10</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 消费最终结果</span></span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> result &#123;</span><br><span class="line">        fmt.Println(v)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight"><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">16</span><br><span class="line">25</span><br><span class="line">36</span><br><span class="line">49</span><br><span class="line">64</span><br><span class="line">81</span><br><span class="line">100</span><br></pre></td></tr></table></figure><p>流水线的每一步都在独立的 goroutine 中运行，通过 channel 连接。数据像水一样从一个阶段流向下一个阶段。</p><p><strong>函数返回 <code>&lt;-chan int</code>（只读 channel）</strong> 是 Go 中流水线的标准写法：每个阶段负责创建 channel、启动 goroutine、返回只读 channel。调用方只管从返回的 channel 中读取。</p><h3 id="10-4-用-channel-做信号通知">10.4 用 channel 做信号通知</h3><p>有时候 channel 不传具体数据，只用来传递&quot;某件事发生了&quot;的信号：</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></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;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">    done := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)  <span class="comment">// 空结构体不占内存</span></span><br><span class="line"></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(<span class="string">&quot;工作中...&quot;</span>)</span><br><span class="line">        time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">        fmt.Println(<span class="string">&quot;工作完成！&quot;</span>)</span><br><span class="line">        <span class="built_in">close</span>(done)  <span class="comment">// 通过关闭 channel 发送信号</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;等待完成...&quot;</span>)</span><br><span class="line">    &lt;-done  <span class="comment">// 阻塞，直到 done 被关闭</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;继续后续操作&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong><code>chan struct&#123;&#125;</code></strong> 是信号通知的惯用类型。<code>struct&#123;&#125;</code> 不占任何内存（大小为 0），表示&quot;我不关心传什么值，只关心有没有信号&quot;。</p><p>用 <code>close(done)</code> 而不是 <code>done &lt;- struct&#123;&#125;&#123;&#125;</code>，好处是：关闭 channel 后，<strong>所有</strong>在 <code>&lt;-done</code> 上等待的 goroutine 都会被唤醒。这是一种广播机制。</p><hr><h2 id="11-常见坑总结">11. 常见坑总结</h2><h3 id="11-1-往已关闭的-channel-发送数据——panic">11.1 往已关闭的 channel 发送数据——panic</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"><span class="built_in">close</span>(ch)</span><br><span class="line">ch &lt;- <span class="number">1</span>  <span class="comment">// panic: send on closed channel</span></span><br></pre></td></tr></table></figure><p><strong>这是最常见的 channel 错误</strong>。记住：关闭 channel 是发送方的事，接收方永远不要关闭。</p><h3 id="11-2-关闭已关闭的-channel——panic">11.2 关闭已关闭的 channel——panic</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"><span class="built_in">close</span>(ch)</span><br><span class="line"><span class="built_in">close</span>(ch)  <span class="comment">// panic: close of closed channel</span></span><br></pre></td></tr></table></figure><p>不要重复关闭 channel。</p><h3 id="11-3-死锁——所有-goroutine-都在等待">11.3 死锁——所有 goroutine 都在等待</h3><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="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line">    ch &lt;- <span class="number">1</span>  <span class="comment">// 阻塞，没有其他 goroutine 来接收</span></span><br><span class="line">    fmt.Println(&lt;-ch)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// fatal error: all goroutines are asleep - deadlock!</span></span><br></pre></td></tr></table></figure><p>无缓冲 channel 在同一个 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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 有缓冲的 channel 可以在同一个 goroutine 中操作（只要不超过缓冲区）</span></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">1</span>)  <span class="comment">// 容量为 1</span></span><br><span class="line">    ch &lt;- <span class="number">1</span>   <span class="comment">// 不阻塞（缓冲区未满）</span></span><br><span class="line">    fmt.Println(&lt;-ch)  <span class="comment">// 1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="11-4-忘记关闭-channel-导致-range-卡死">11.4 忘记关闭 channel 导致 range 卡死</h3><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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"></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">        ch &lt;- <span class="number">1</span></span><br><span class="line">        ch &lt;- <span class="number">2</span></span><br><span class="line">        <span class="comment">// 忘了 close(ch)</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> v := <span class="keyword">range</span> ch &#123;</span><br><span class="line">        fmt.Println(v)  <span class="comment">// 输出 1、2 后永远阻塞</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>for range</code> 会一直等到 channel 关闭才停止。不关闭就会变成死锁。</p><h3 id="11-5-只发不收——goroutine-泄漏">11.5 只发不收——goroutine 泄漏</h3><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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>)</span><br><span class="line"></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">        ch &lt;- <span class="number">42</span>  <span class="comment">// 发送后一直等待接收方，但没人来收</span></span><br><span class="line">    &#125;()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// main 直接返回了，没有 &lt;-ch</span></span><br><span class="line">    <span class="comment">// goroutine 永远阻塞在 ch &lt;- 42，泄漏了</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个往 channel 发送的数据都要有人接收。如果没人收，发送方的 goroutine 会永远阻塞。</p><h3 id="11-6-nil-channel-的用途">11.6 nil channel 的用途</h3><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">var</span> ch <span class="keyword">chan</span> <span class="type">int</span>  <span class="comment">// nil</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ch &lt;- 1   // 永久阻塞</span></span><br><span class="line"><span class="comment">// &lt;-ch      // 永久阻塞</span></span><br><span class="line"><span class="comment">// close(ch) // panic!</span></span><br></pre></td></tr></table></figure><p>nil channel 看起来没用，但在 <code>select</code>（下一课）中有特殊用途——可以动态禁用某个 case。</p><h3 id="11-7-缓冲区大小不是越大越好">11.7 缓冲区大小不是越大越好</h3><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">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">1000000</span>)  <span class="comment">// 不要随便设一百万</span></span><br></pre></td></tr></table></figure><p>缓冲区太大会掩盖问题——如果消费者比生产者慢，大缓冲区只是延迟了阻塞，内存会越积越多。合理的缓冲区大小通常是个位数到几十。</p><hr><h2 id="12-本课练习">12. 本课练习</h2><h3 id="练习-1：乒乓球">练习 1：乒乓球</h3><p>要求：</p><ul><li>两个 goroutine 模拟乒乓球对打</li><li>用一个 channel 传递&quot;球&quot;（一个 int，代表击球次数）</li><li>每次接到球后次数加 1，打印 <code>&quot;Player X 击球 N 次&quot;</code> 后把球传回去</li><li>打到 10 次后结束</li></ul><hr><h3 id="练习-2：扇出（Fan-out）">练习 2：扇出（Fan-out）</h3><p>要求：</p><ul><li>一个生产者 goroutine 生成 1~20 的数字，发送到 channel</li><li>启动 3 个消费者 goroutine 从同一个 channel 接收并打印</li><li>观察哪个消费者接收了哪些数字</li></ul><p>提示：多个 goroutine 从同一个 channel 接收，每条数据只会被一个 goroutine 拿到。</p><hr><h3 id="练习-3：求和流水线">练习 3：求和流水线</h3><p>要求：</p><ul><li>第一阶段：生成 1~100 的数字</li><li>第二阶段：过滤出偶数</li><li>第三阶段：累加所有偶数</li><li>每个阶段一个 goroutine，用 channel 连接</li><li>最后输出结果（应该是 2550）</li></ul><hr><h3 id="练习-4：超时处理（预习）">练习 4：超时处理（预习）</h3><p>要求：</p><ul><li>启动一个 goroutine，模拟一个可能很慢的操作（随机 1~5 秒）</li><li>主 goroutine 最多等 2 秒</li><li>如果 2 秒内拿到结果就打印，否则打印&quot;超时&quot;</li></ul><p>提示：用 <code>time.After(2 * time.Second)</code> 和 <code>select</code>（这是下一课的内容，先尝试自己查资料实现）。</p><hr><h3 id="练习-5：安全计数器">练习 5：安全计数器</h3><p>要求：</p><ul><li>启动 100 个 goroutine，每个 goroutine 往 channel 发送 1</li><li>主 goroutine 从 channel 接收 100 个值并累加</li><li>输出最终结果（应该是 100）</li></ul><hr><h2 id="13-自测题">13. 自测题</h2><h3 id="13-1-概念题">13.1 概念题</h3><ol><li>channel 的零值是什么？对 nil channel 发送会怎样？</li><li>无缓冲 channel 和有缓冲 channel 的阻塞规则分别是什么？</li><li>关闭 channel 后，发送和接收分别会怎样？</li><li><code>v, ok := &lt;-ch</code> 中 <code>ok</code> 为 <code>false</code> 代表什么？</li><li>为什么只有发送方应该关闭 channel？</li><li><code>for range ch</code> 什么时候结束？</li><li><code>chan struct&#123;&#125;</code> 是什么意思？什么时候用？</li><li><code>chan&lt;- int</code> 和 <code>&lt;-chan int</code> 分别是什么？</li></ol><h3 id="13-2-代码阅读题">13.2 代码阅读题</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><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></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">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="type">int</span>, <span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">    ch &lt;- <span class="number">1</span></span><br><span class="line">    ch &lt;- <span class="number">2</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(&lt;-ch)</span><br><span class="line"></span><br><span class="line">    ch &lt;- <span class="number">3</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(&lt;-ch)</span><br><span class="line">    fmt.Println(&lt;-ch)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><figure class="highlight"><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">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td></tr></table></figure><p>解释：</p><ol><li><code>ch &lt;- 1</code>：缓冲区 [1]</li><li><code>ch &lt;- 2</code>：缓冲区 [1, 2]</li><li><code>&lt;-ch</code>：取出 1，缓冲区 [2]，打印 1</li><li><code>ch &lt;- 3</code>：缓冲区 [2, 3]（不会阻塞，因为刚取了一个）</li><li><code>&lt;-ch</code>：取出 2，打印 2</li><li><code>&lt;-ch</code>：取出 3，打印 3</li></ol><p>channel 是 FIFO（先进先出），所以输出顺序就是发送顺序。</p></details><hr><h2 id="14-本课总结">14. 本课总结</h2><p>这一课你学到了 Go 并发编程最核心的通信机制——channel。</p><table><thead><tr><th>场景</th><th>做法</th></tr></thead><tbody><tr><td>创建 channel</td><td><code>make(chan T)</code> 或 <code>make(chan T, n)</code></td></tr><tr><td>发送 / 接收</td><td><code>ch &lt;- v</code> / <code>v := &lt;-ch</code></td></tr><tr><td>关闭 channel</td><td><code>close(ch)</code>（发送方关闭）</td></tr><tr><td>遍历 channel</td><td><code>for v := range ch</code>（需要关闭才能结束）</td></tr><tr><td>判断是否关闭</td><td><code>v, ok := &lt;-ch</code>，<code>ok == false</code> 表示已关闭</td></tr><tr><td>信号通知</td><td><code>chan struct&#123;&#125;</code>，用 <code>close</code> 广播</td></tr><tr><td>限制方向</td><td><code>chan&lt;- T</code>（只发送）、<code>&lt;-chan T</code>（只接收）</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>channel 是 goroutine 之间传递数据的管道——发送和接收是并发安全的</strong></li><li><strong>无缓冲 channel 是同步的，有缓冲 channel 有队列——按需选择</strong></li><li><strong>只有发送方关闭 channel——往已关闭的 channel 发送会 panic</strong></li></ol><hr><h2 id="15-下一课预告">15. 下一课预告</h2><p>这一课你学会了用 channel 在两个 goroutine 之间传递数据。但如果一个 goroutine 需要<strong>同时监听多个 channel</strong>呢？</p><ul><li>同时等待用户输入和超时信号</li><li>同时等待多个服务的响应</li><li>处理&quot;哪个先来就先处理哪个&quot;的场景</li></ul><p>下一课：<strong><code>select</code> 与并发控制</strong></p><p>会重点讲：</p><ul><li><code>select</code> 是什么——channel 的多路复用器</li><li>怎么用 <code>select</code> 同时监听多个 channel</li><li>超时处理——<code>time.After</code> 配合 <code>select</code></li><li><code>default</code> 分支——非阻塞操作</li><li>常见的 <code>select</code> 使用模式</li></ul><p>学完下一课，你就能处理更复杂的并发场景了。</p>]]></content>
    
    
    <summary type="html">Go语言系列26</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 25 课：goroutine 入门</title>
    <link href="https://yjyrichard.github.io/posts/26f3fbe0.html"/>
    <id>https://yjyrichard.github.io/posts/26f3fbe0.html</id>
    <published>2026-03-22T15:28:11.717Z</published>
    <updated>2026-03-22T15:40:40.011Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 25 课：goroutine 入门</h1><blockquote><p>学习定位：这是整套 Go 教程的第 25 课，也是阶段五（并发与工程阶段）的第一课。<br>前置要求：已经完成第 24 课，具备用 Go 编写完整小项目的能力。需要熟悉函数、切片、循环等基础知识。<br>本课目标：理解 Go 并发的基本概念，能创建和运行 goroutine，理解主协程退出问题并掌握基本的等待方式，能初步感受并发编程的威力和复杂性。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>到目前为止，你写的所有程序都是<strong>顺序执行</strong>的——一行一行往下跑，做完一件事才做下一件事。但现实世界不是这样运转的：</p><ul><li>一个 Web 服务器要<strong>同时</strong>处理多个用户的请求</li><li>一个下载器要<strong>同时</strong>下载多个文件</li><li>一个聊天程序要<strong>同时</strong>发送和接收消息</li></ul><p>这就是<strong>并发</strong>要解决的问题。而 Go 语言最大的卖点之一，就是它把并发做得<strong>极其简单</strong>。</p><p>你需要搞明白以下问题：</p><ul><li>什么是并发，和并行有什么区别</li><li>goroutine 是什么</li><li>怎么创建一个 goroutine</li><li>为什么我的 goroutine “没有执行”</li><li>怎么等 goroutine 执行完</li><li>goroutine 和线程有什么区别</li><li>开多少个 goroutine 合适</li></ul><p>学完这一课，你就迈进了 Go 最强大也最有意思的领域。</p><hr><h2 id="2-先搞清楚概念：并发-vs-并行">2. 先搞清楚概念：并发 vs 并行</h2><p>这两个词经常被混用，但它们不是一回事。</p><h3 id="2-1-并发（Concurrency）">2.1 并发（Concurrency）</h3><p><strong>并发是一种程序设计方式</strong>——把任务拆成可以独立推进的部分。</p><p>想象一个厨师做饭：先把水烧上，等水开的时候去切菜，切完菜再去下面条。一个人，但三件事在&quot;同时&quot;推进。这就是并发——<strong>同一时间段内处理多件事</strong>。</p><h3 id="2-2-并行（Parallelism）">2.2 并行（Parallelism）</h3><p><strong>并行是真正的同时执行</strong>——多个 CPU 核心各干各的。</p><p>两个厨师，一个切菜，一个烧水。两件事在<strong>同一时刻</strong>同时发生。这才是并行。</p><h3 id="2-3-关系">2.3 关系</h3><figure class="highlight"><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><br><span class="line">并行：多个人同时干活（真正同时）</span><br></pre></td></tr></table></figure><p>Go 的 goroutine 是<strong>并发</strong>的设计工具。如果你的机器有多个 CPU 核心，Go 的运行时会自动把 goroutine 分配到不同核心上<strong>并行执行</strong>。但你写代码时，只需要关心并发设计——怎么拆任务，Go 的运行时帮你搞定并行。</p><hr><h2 id="3-goroutine-是什么">3. goroutine 是什么</h2><h3 id="3-1-一句话定义">3.1 一句话定义</h3><p><strong>goroutine 是 Go 中的轻量级执行单元</strong>。你可以把它理解为一个超轻量的&quot;线程&quot;。</p><h3 id="3-2-你已经用过一个-goroutine-了">3.2 你已经用过一个 goroutine 了</h3><p>每个 Go 程序启动时，<code>main</code> 函数就运行在一个 goroutine 里——叫做<strong>主 goroutine</strong>。之前写的所有程序都是只有这一个 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></pre></td><td class="code"><pre><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;我在主 goroutine 中运行&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这一课要学的就是：怎么创建更多的 goroutine。</p><hr><h2 id="4-创建你的第一个-goroutine">4. 创建你的第一个 goroutine</h2><h3 id="4-1-go-关键字">4.1 <code>go</code> 关键字</h3><p>创建 goroutine 只需要一个关键字：<strong><code>go</code></strong>。在函数调用前面加 <code>go</code>，这个函数就会在一个新的 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><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></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">sayHello</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Hello from goroutine!&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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">go</span> sayHello()  <span class="comment">// 在新的 goroutine 中执行 sayHello</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Hello from main!&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行一下：</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hello <span class="selector-tag">from</span> <span class="selector-tag">main</span>!</span><br></pre></td></tr></table></figure><p><strong>等等，<code>sayHello</code> 的输出呢？</strong></p><h3 id="4-2-第一个大坑：主-goroutine-退出，程序就结束了">4.2 第一个大坑：主 goroutine 退出，程序就结束了</h3><p>这是学 goroutine 必须搞懂的第一件事：</p><p><strong>当 <code>main</code> 函数返回时，程序立即结束，不管其他 goroutine 有没有执行完。</strong></p><p>上面的代码中，<code>go sayHello()</code> 启动了一个新的 goroutine，但 <code>main</code> 函数紧接着就执行了 <code>fmt.Println(&quot;Hello from main!&quot;)</code>，然后 <code>main</code> 返回，程序结束。新的 goroutine 还没来得及执行就被&quot;杀&quot;了。</p><p>这就像一个公司的老板（主 goroutine）下班走人了，员工（其他 goroutine）不管手里的活干没干完，公司直接关门。</p><h3 id="4-3-临时方案：用-time-Sleep-等一等">4.3 临时方案：用 <code>time.Sleep</code> 等一等</h3><p>最粗暴的办法是让主 goroutine 睡一会儿，给其他 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><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></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;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">sayHello</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;Hello from goroutine!&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">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">go</span> sayHello()</span><br><span class="line"></span><br><span class="line">    time.Sleep(<span class="number">100</span> * time.Millisecond)  <span class="comment">// 等 100 毫秒</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;Hello from main!&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight css"><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">Hello <span class="selector-tag">from</span> goroutine!</span><br><span class="line">Hello <span class="selector-tag">from</span> <span class="selector-tag">main</span>!</span><br></pre></td></tr></table></figure><p>这次看到 goroutine 的输出了。但<strong>这是一个很烂的解决方案</strong>：</p><ul><li>你不知道 goroutine 需要多少时间才能执行完</li><li>睡少了——goroutine 还没执行完</li><li>睡多了——白白浪费时间</li><li>生产环境绝对不能用这种方式</li></ul><p>但是作为学习工具，<code>time.Sleep</code> 能帮你直观地看到 goroutine 确实在运行。后面我们马上学正确的等待方式。</p><hr><h2 id="5-启动多个-goroutine">5. 启动多个 goroutine</h2><h3 id="5-1-看看多个-goroutine-交替执行">5.1 看看多个 goroutine 交替执行</h3><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></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;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">printNumbers</span><span class="params">(name <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;[%s] %d\n&quot;</span>, name, i)</span><br><span class="line">        time.Sleep(<span class="number">50</span> * time.Millisecond)  <span class="comment">// 模拟耗时操作</span></span><br><span class="line">    &#125;</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">go</span> printNumbers(<span class="string">&quot;A&quot;</span>)</span><br><span class="line">    <span class="keyword">go</span> printNumbers(<span class="string">&quot;B&quot;</span>)</span><br><span class="line">    <span class="keyword">go</span> printNumbers(<span class="string">&quot;C&quot;</span>)</span><br><span class="line"></span><br><span class="line">    time.Sleep(<span class="number">500</span> * time.Millisecond)</span><br><span class="line">    fmt.Println(<span class="string">&quot;全部完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出（每次运行可能不同）：</p><figure class="highlight angelscript"><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></pre></td><td class="code"><pre><span class="line"><span class="string">[C]</span> <span class="number">1</span></span><br><span class="line"><span class="string">[A]</span> <span class="number">1</span></span><br><span class="line"><span class="string">[B]</span> <span class="number">1</span></span><br><span class="line"><span class="string">[A]</span> <span class="number">2</span></span><br><span class="line"><span class="string">[C]</span> <span class="number">2</span></span><br><span class="line"><span class="string">[B]</span> <span class="number">2</span></span><br><span class="line"><span class="string">[B]</span> <span class="number">3</span></span><br><span class="line"><span class="string">[C]</span> <span class="number">3</span></span><br><span class="line"><span class="string">[A]</span> <span class="number">3</span></span><br><span class="line"><span class="string">[A]</span> <span class="number">4</span></span><br><span class="line"><span class="string">[B]</span> <span class="number">4</span></span><br><span class="line"><span class="string">[C]</span> <span class="number">4</span></span><br><span class="line"><span class="string">[C]</span> <span class="number">5</span></span><br><span class="line"><span class="string">[A]</span> <span class="number">5</span></span><br><span class="line"><span class="string">[B]</span> <span class="number">5</span></span><br><span class="line">全部完成</span><br></pre></td></tr></table></figure><p><strong>关键观察</strong>：</p><ol><li>A、B、C 的输出是<strong>交错的</strong>——它们在并发执行</li><li>每次运行的<strong>顺序可能不同</strong>——这是并发的本质特征</li><li>三个任务本来需要 3 × 250ms = 750ms，但并发执行只用了约 250ms</li></ol><h3 id="5-2-顺序执行-vs-并发执行的对比">5.2 顺序执行 vs 并发执行的对比</h3><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></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;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">task</span><span class="params">(name <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s 开始\n&quot;</span>, name)</span><br><span class="line">    time.Sleep(<span class="number">200</span> * time.Millisecond)  <span class="comment">// 模拟耗时操作</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s 结束\n&quot;</span>, name)</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="comment">// 顺序执行</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 顺序执行 ===&quot;</span>)</span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    task(<span class="string">&quot;任务1&quot;</span>)</span><br><span class="line">    task(<span class="string">&quot;任务2&quot;</span>)</span><br><span class="line">    task(<span class="string">&quot;任务3&quot;</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;耗时: %v\n\n&quot;</span>, time.Since(start))</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 并发执行</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 并发执行 ===&quot;</span>)</span><br><span class="line">    start = time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> task(<span class="string">&quot;任务1&quot;</span>)</span><br><span class="line">    <span class="keyword">go</span> task(<span class="string">&quot;任务2&quot;</span>)</span><br><span class="line">    <span class="keyword">go</span> task(<span class="string">&quot;任务3&quot;</span>)</span><br><span class="line"></span><br><span class="line">    time.Sleep(<span class="number">300</span> * time.Millisecond)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;耗时: %v\n&quot;</span>, time.Since(start))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight asciidoc"><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></pre></td><td class="code"><pre><span class="line"><span class="section">=== 顺序执行 ===</span></span><br><span class="line">任务1 开始</span><br><span class="line">任务1 结束</span><br><span class="line">任务2 开始</span><br><span class="line">任务2 结束</span><br><span class="line">任务3 开始</span><br><span class="line">任务3 结束</span><br><span class="line">耗时: 600ms</span><br><span class="line"></span><br><span class="line"><span class="section">=== 并发执行 ===</span></span><br><span class="line">任务3 开始</span><br><span class="line">任务1 开始</span><br><span class="line">任务2 开始</span><br><span class="line">任务1 结束</span><br><span class="line">任务2 结束</span><br><span class="line">任务3 结束</span><br><span class="line">耗时: 300ms</span><br></pre></td></tr></table></figure><p>顺序执行 600ms，并发执行约 300ms——<strong>快了一倍</strong>。这就是并发的价值：多件事同时推进，总时间取决于最慢的那一个。</p><hr><h2 id="6-正确的等待方式：sync-WaitGroup">6. 正确的等待方式：<code>sync.WaitGroup</code></h2><p><code>time.Sleep</code> 是猜时间，不靠谱。Go 标准库提供了 <code>sync.WaitGroup</code>，让你精确地等待一组 goroutine 执行完毕。</p><h3 id="6-1-基本用法">6.1 基本用法</h3><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;fmt&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">worker</span><span class="params">(id <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()  <span class="comment">// 执行完毕，计数器减 1</span></span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 开始\n&quot;</span>, id)</span><br><span class="line">    time.Sleep(<span class="number">100</span> * time.Millisecond)  <span class="comment">// 模拟耗时操作</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 完成\n&quot;</span>, id)</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">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)           <span class="comment">// 计数器加 1</span></span><br><span class="line">        <span class="keyword">go</span> worker(i, &amp;wg)   <span class="comment">// 启动 goroutine</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()  <span class="comment">// 阻塞，直到计数器归零</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;所有 Worker 执行完毕&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight tap"><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">Worker<span class="number"> 3 </span>开始</span><br><span class="line">Worker<span class="number"> 1 </span>开始</span><br><span class="line">Worker<span class="number"> 2 </span>开始</span><br><span class="line">Worker<span class="number"> 1 </span>完成</span><br><span class="line">Worker<span class="number"> 3 </span>完成</span><br><span class="line">Worker<span class="number"> 2 </span>完成</span><br><span class="line">所有 Worker 执行完毕</span><br></pre></td></tr></table></figure><h3 id="6-2-WaitGroup-三步曲">6.2 WaitGroup 三步曲</h3><p><code>sync.WaitGroup</code> 就三个方法，像一个计数器：</p><table><thead><tr><th>方法</th><th>作用</th><th>什么时候调用</th></tr></thead><tbody><tr><td><code>wg.Add(n)</code></td><td>计数器加 n</td><td>启动 goroutine <strong>之前</strong></td></tr><tr><td><code>wg.Done()</code></td><td>计数器减 1</td><td>goroutine <strong>结束时</strong></td></tr><tr><td><code>wg.Wait()</code></td><td>阻塞等待，直到计数器变为 0</td><td>主 goroutine 中</td></tr></tbody></table><p>工作流程：</p><figure class="highlight stylus"><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"><span class="function"><span class="title">Add</span><span class="params">(<span class="number">1</span>)</span></span> → 计数器: <span class="number">1</span>    启动第 <span class="number">1</span> 个 goroutine</span><br><span class="line"><span class="function"><span class="title">Add</span><span class="params">(<span class="number">1</span>)</span></span> → 计数器: <span class="number">2</span>    启动第 <span class="number">2</span> 个 goroutine</span><br><span class="line"><span class="function"><span class="title">Add</span><span class="params">(<span class="number">1</span>)</span></span> → 计数器: <span class="number">3</span>    启动第 <span class="number">3</span> 个 goroutine</span><br><span class="line"><span class="function"><span class="title">Wait</span><span class="params">()</span></span> → 阻塞...</span><br><span class="line"><span class="function"><span class="title">Done</span><span class="params">()</span></span> → 计数器: <span class="number">2</span>    第 X 个 goroutine 完成</span><br><span class="line"><span class="function"><span class="title">Done</span><span class="params">()</span></span> → 计数器: <span class="number">1</span>    第 Y 个 goroutine 完成</span><br><span class="line"><span class="function"><span class="title">Done</span><span class="params">()</span></span> → 计数器: <span class="number">0</span>    第 Z 个 goroutine 完成</span><br><span class="line"><span class="function"><span class="title">Wait</span><span class="params">()</span></span> → 解除阻塞     继续执行</span><br></pre></td></tr></table></figure><h3 id="6-3-defer-wg-Done-的重要性">6.3 <code>defer wg.Done()</code> 的重要性</h3><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="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(id <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()  <span class="comment">// 用 defer 确保无论如何都会调用 Done</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 即使中间 return 或 panic，defer 也会执行</span></span><br><span class="line">    <span class="keyword">if</span> id == <span class="number">2</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;Worker %d 跳过\n&quot;</span>, id)</span><br><span class="line">        <span class="keyword">return</span>  <span class="comment">// 提前返回，defer wg.Done() 仍然会执行</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;Worker %d 工作中\n&quot;</span>, id)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么用 <code>defer</code></strong>（第 17 课）：如果函数有多个 return 路径，或者中间可能 panic，用 <code>defer wg.Done()</code> 保证计数器一定会减 1。如果忘了调 <code>Done()</code>，<code>Wait()</code> 会永远阻塞，程序就卡死了。</p><h3 id="6-4-WaitGroup-必须传指针">6.4 WaitGroup 必须传指针</h3><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="comment">// 错误：传值，worker 里操作的是副本</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(id <span class="type">int</span>, wg sync.WaitGroup)</span></span> &#123;  <span class="comment">// 值传递！</span></span><br><span class="line">    <span class="keyword">defer</span> wg.Done()  <span class="comment">// 操作的是副本，对原始 WaitGroup 没有影响</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：传指针</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(id <span class="type">int</span>, wg *sync.WaitGroup)</span></span> &#123;  <span class="comment">// 指针传递</span></span><br><span class="line">    <span class="keyword">defer</span> wg.Done()  <span class="comment">// 操作的是同一个 WaitGroup</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>sync.WaitGroup</code> 是一个结构体，传值会拷贝一份（第 12 课）。你在副本上调 <code>Done()</code>，原始的计数器不会变，<code>Wait()</code> 永远等不到。</p><hr><h2 id="7-匿名函数-goroutine">7. 匿名函数 goroutine</h2><h3 id="7-1-基本写法">7.1 基本写法</h3><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><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></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;sync&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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            fmt.Printf(<span class="string">&quot;goroutine %d 执行\n&quot;</span>, id)</span><br><span class="line">        &#125;(i)  <span class="comment">// 注意：把 i 作为参数传进去</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;完成&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出（顺序不确定）：</p><figure class="highlight tap"><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">goroutine<span class="number"> 3 </span>执行</span><br><span class="line">goroutine<span class="number"> 1 </span>执行</span><br><span class="line">goroutine<span class="number"> 2 </span>执行</span><br><span class="line">完成</span><br></pre></td></tr></table></figure><h3 id="7-2-第二个大坑：闭包变量捕获">7.2 第二个大坑：闭包变量捕获</h3><p><strong>这是 Go 并发编程中最经典的 bug</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><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;sync&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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</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 class="comment">// 没有传参数</span></span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            fmt.Printf(<span class="string">&quot;goroutine %d 执行\n&quot;</span>, i)  <span class="comment">// 直接用外层的 i</span></span><br><span class="line">        &#125;()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你可能期望输出 1、2、3，但实际输出很可能是：</p><figure class="highlight apache"><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="attribute">goroutine</span> <span class="number">4</span> 执行</span><br><span class="line"><span class="attribute">goroutine</span> <span class="number">4</span> 执行</span><br><span class="line"><span class="attribute">goroutine</span> <span class="number">4</span> 执行</span><br></pre></td></tr></table></figure><p><strong>为什么全是 4？</strong></p><ol><li><code>for</code> 循环执行得非常快，三个 goroutine 被创建出来了，但还没来得及运行</li><li>循环结束时 <code>i</code> 的值已经变成了 4（<code>i++</code> 之后不满足 <code>i &lt;= 3</code>，退出循环）</li><li>三个 goroutine 开始执行，它们都去读 <code>i</code> 的值——此时 <code>i</code> 已经是 4 了</li><li>三个 goroutine <strong>共享同一个变量 <code>i</code></strong>，这就是闭包的特性</li></ol><p><strong>解决办法</strong>：把变量作为参数传进去，每个 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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 方法一：作为参数传入（推荐）</span></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;goroutine %d 执行\n&quot;</span>, id)</span><br><span class="line">&#125;(i)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 方法二：在循环内创建局部变量</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">3</span>; i++ &#123;</span><br><span class="line">    id := i  <span class="comment">// 每次循环创建一个新变量</span></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.Printf(<span class="string">&quot;goroutine %d 执行\n&quot;</span>, id)</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两种都可以，第一种更明确。</p><hr><h2 id="8-goroutine-有多轻量">8. goroutine 有多轻量</h2><h3 id="8-1-创建大量-goroutine">8.1 创建大量 goroutine</h3><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></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;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">    count := <span class="number">100000</span>  <span class="comment">// 十万个 goroutine</span></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; count; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            <span class="comment">// 每个 goroutine 做一点简单的事</span></span><br><span class="line">            _ = id * id</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;创建并完成 %d 个 goroutine，耗时: %v\n&quot;</span>, count, time.Since(start))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在普通电脑上，十万个 goroutine 通常在几百毫秒内就能创建并完成。</p><h3 id="8-2-goroutine-vs-线程">8.2 goroutine vs 线程</h3><table><thead><tr><th>特性</th><th>goroutine</th><th>操作系统线程</th></tr></thead><tbody><tr><td>初始栈大小</td><td>约 2KB</td><td>约 1MB</td></tr><tr><td>创建开销</td><td>极低（微秒级）</td><td>较高（毫秒级）</td></tr><tr><td>能创建的数量</td><td>轻松十万级</td><td>通常几千就很多了</td></tr><tr><td>调度方式</td><td>Go 运行时调度（用户态）</td><td>操作系统调度（内核态）</td></tr><tr><td>切换开销</td><td>很低</td><td>较高</td></tr></tbody></table><p><strong>关键区别</strong>：goroutine 不是线程。Go 运行时会把成千上万个 goroutine 映射到少量的操作系统线程上（默认等于 CPU 核心数）。goroutine 的调度完全在用户态完成，不需要进入操作系统内核，所以非常快。</p><h3 id="8-3-goroutine-的栈是动态增长的">8.3 goroutine 的栈是动态增长的</h3><figure class="highlight abnf"><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">操作系统线程：</span><br><span class="line">  创建时分配 <span class="number">1</span>MB 栈空间（固定大小）</span><br><span class="line">  <span class="number">1</span>万个线程 <span class="operator">=</span> <span class="number">10</span>GB 内存</span><br><span class="line"></span><br><span class="line">goroutine：</span><br><span class="line">  创建时只分配约 <span class="number">2</span>KB 栈空间</span><br><span class="line">  需要更多时自动增长（最大 <span class="number">1</span>GB）</span><br><span class="line">  <span class="number">1</span>万个 goroutine ≈ <span class="number">20</span>MB 内存</span><br></pre></td></tr></table></figure><p>这就是为什么 Go 能轻松创建大量 goroutine——内存占用非常小。</p><hr><h2 id="9-goroutine-的执行顺序">9. goroutine 的执行顺序</h2><h3 id="9-1-顺序不确定">9.1 顺序不确定</h3><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;sync&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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt;= <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            fmt.Println(id)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行多次，你会看到不同的输出顺序：</p><figure class="highlight tap"><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="number"> 5 </span>3<span class="number"> 1 </span>2 4</span><br><span class="line">第二次:<span class="number"> 1 </span>5<span class="number"> 2 </span>4 3</span><br><span class="line">第三次:<span class="number"> 3 </span>1<span class="number"> 5 </span>4 2</span><br></pre></td></tr></table></figure><p><strong>并发程序的执行顺序是不确定的</strong>。如果你的程序依赖 goroutine 的执行顺序，那说明设计有问题。goroutine 之间的协调要通过同步机制（WaitGroup、channel 等），不能靠&quot;碰巧的执行顺序&quot;。</p><h3 id="9-2-不能假设先启动的先执行">9.2 不能假设先启动的先执行</h3><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="keyword">go</span> funcA()  <span class="comment">// 先启动</span></span><br><span class="line"><span class="keyword">go</span> funcB()  <span class="comment">// 后启动</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 不能假设 funcA 一定比 funcB 先执行</span></span><br><span class="line"><span class="comment">// 也不能假设 funcB 一定比 funcA 先执行</span></span><br><span class="line"><span class="comment">// 谁先执行完全由 Go 运行时决定</span></span><br></pre></td></tr></table></figure><hr><h2 id="10-有返回值的函数怎么办">10. 有返回值的函数怎么办</h2><p>goroutine 启动的函数<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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误：go 语句没法接收返回值</span></span><br><span class="line">result := <span class="keyword">go</span> computeSomething()  <span class="comment">// 编译错误！</span></span><br></pre></td></tr></table></figure><p>这是因为 <code>go</code> 启动的函数是异步执行的，调用方不会等它完成，自然也没法立刻拿到返回值。</p><p><strong>那怎么拿到 goroutine 的执行结果？</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><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></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;sync&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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">    results := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">5</span>)  <span class="comment">// 预分配好结果切片</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">5</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(idx <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            results[idx] = idx * idx  <span class="comment">// 每个 goroutine 写自己的位置</span></span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(results)  <span class="comment">// [0 1 4 9 16]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里能正常工作是因为每个 goroutine 写的是 <code>results</code> 中不同的索引位置，不存在竞争。</p><p>但如果多个 goroutine 要写<strong>同一个变量</strong>，就有问题了——这叫<strong>数据竞争</strong>（data race）。下一课会讲用 channel 来安全地传递数据，第 28 课会讲用 Mutex 来保护共享数据。</p><hr><h2 id="11-实战示例">11. 实战示例</h2><h3 id="11-1-并发下载模拟器">11.1 并发下载模拟器</h3><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><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></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;math/rand&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="keyword">type</span> DownloadResult <span class="keyword">struct</span> &#123;</span><br><span class="line">    URL      <span class="type">string</span></span><br><span class="line">    Size     <span class="type">int</span></span><br><span class="line">    Duration time.Duration</span><br><span class="line">    Err      <span class="type">error</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">download</span><span class="params">(url <span class="type">string</span>)</span></span> DownloadResult &#123;</span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟下载耗时（100ms ~ 500ms 随机）</span></span><br><span class="line">    duration := time.Duration(<span class="number">100</span>+rand.Intn(<span class="number">400</span>)) * time.Millisecond</span><br><span class="line">    time.Sleep(duration)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟下载结果</span></span><br><span class="line">    size := <span class="number">1024</span> + rand.Intn(<span class="number">9216</span>)  <span class="comment">// 1KB ~ 10KB</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> DownloadResult&#123;</span><br><span class="line">        URL:      url,</span><br><span class="line">        Size:     size,</span><br><span class="line">        Duration: time.Since(start),</span><br><span class="line">    &#125;</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">    urls := []<span class="type">string</span>&#123;</span><br><span class="line">        <span class="string">&quot;https://example.com/file1.txt&quot;</span>,</span><br><span class="line">        <span class="string">&quot;https://example.com/file2.txt&quot;</span>,</span><br><span class="line">        <span class="string">&quot;https://example.com/file3.txt&quot;</span>,</span><br><span class="line">        <span class="string">&quot;https://example.com/file4.txt&quot;</span>,</span><br><span class="line">        <span class="string">&quot;https://example.com/file5.txt&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// === 顺序下载 ===</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 顺序下载 ===&quot;</span>)</span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, url := <span class="keyword">range</span> urls &#123;</span><br><span class="line">        result := download(url)</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: %dKB, 耗时 %v\n&quot;</span>, result.URL, result.Size/<span class="number">1024</span>, result.Duration)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    seqDuration := time.Since(start)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;顺序下载总耗时: %v\n\n&quot;</span>, seqDuration)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// === 并发下载 ===</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 并发下载 ===&quot;</span>)</span><br><span class="line">    start = time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">    results := <span class="built_in">make</span>([]DownloadResult, <span class="built_in">len</span>(urls))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i, url := <span class="keyword">range</span> urls &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(idx <span class="type">int</span>, u <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            results[idx] = download(u)</span><br><span class="line">        &#125;(i, url)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, result := <span class="keyword">range</span> results &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: %dKB, 耗时 %v\n&quot;</span>, result.URL, result.Size/<span class="number">1024</span>, result.Duration)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    concDuration := time.Since(start)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;并发下载总耗时: %v\n&quot;</span>, concDuration)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;加速比: %.1fx\n&quot;</span>, <span class="type">float64</span>(seqDuration)/<span class="type">float64</span>(concDuration))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight asciidoc"><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></pre></td><td class="code"><pre><span class="line"><span class="section">=== 顺序下载 ===</span></span><br><span class="line"><span class="code">  file1.txt: 5KB, 耗时 347ms</span></span><br><span class="line"><span class="code">  file2.txt: 2KB, 耗时 156ms</span></span><br><span class="line"><span class="code">  file3.txt: 8KB, 耗时 423ms</span></span><br><span class="line"><span class="code">  file4.txt: 3KB, 耗时 201ms</span></span><br><span class="line"><span class="code">  file5.txt: 6KB, 耗时 389ms</span></span><br><span class="line">顺序下载总耗时: 1.516s</span><br><span class="line"></span><br><span class="line"><span class="section">=== 并发下载 ===</span></span><br><span class="line"><span class="code">  file1.txt: 4KB, 耗时 289ms</span></span><br><span class="line"><span class="code">  file2.txt: 7KB, 耗时 412ms</span></span><br><span class="line"><span class="code">  file3.txt: 1KB, 耗时 134ms</span></span><br><span class="line"><span class="code">  file4.txt: 5KB, 耗时 367ms</span></span><br><span class="line"><span class="code">  file5.txt: 3KB, 耗时 198ms</span></span><br><span class="line">并发下载总耗时: 412ms</span><br><span class="line">加速比: 3.7x</span><br></pre></td></tr></table></figure><p>顺序下载需要所有时间加起来，并发下载只需要最慢那个的时间。5 个任务并发执行，获得了约 3~4 倍的加速。</p><h3 id="11-2-并发计算">11.2 并发计算</h3><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></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;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="comment">// 计算 start 到 end 之间所有整数的和</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">sumRange</span><span class="params">(start, end <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">    sum := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i := start; i &lt;= end; i++ &#123;</span><br><span class="line">        sum += i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> sum</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">    total := <span class="number">1_000_000_000</span>  <span class="comment">// 10 亿</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// === 单 goroutine ===</span></span><br><span class="line">    start := time.Now()</span><br><span class="line">    result := sumRange(<span class="number">1</span>, total)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;单 goroutine: 结果=%d, 耗时=%v\n&quot;</span>, result, time.Since(start))</span><br><span class="line"></span><br><span class="line">    <span class="comment">// === 4 个 goroutine 分段计算 ===</span></span><br><span class="line">    start = time.Now()</span><br><span class="line">    numWorkers := <span class="number">4</span></span><br><span class="line">    chunkSize := total / numWorkers</span><br><span class="line">    partialResults := <span class="built_in">make</span>([]<span class="type">int</span>, numWorkers)</span><br><span class="line"></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">0</span>; i &lt; numWorkers; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(idx <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            s := idx*chunkSize + <span class="number">1</span></span><br><span class="line">            e := (idx + <span class="number">1</span>) * chunkSize</span><br><span class="line">            <span class="keyword">if</span> idx == numWorkers<span class="number">-1</span> &#123;</span><br><span class="line">                e = total  <span class="comment">// 最后一段包含余数</span></span><br><span class="line">            &#125;</span><br><span class="line">            partialResults[idx] = sumRange(s, e)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 汇总结果</span></span><br><span class="line">    finalResult := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> partialResults &#123;</span><br><span class="line">        finalResult += r</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;4 goroutine:  结果=%d, 耗时=%v\n&quot;</span>, finalResult, time.Since(start))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子展示了<strong>分治思想</strong>：把一个大计算拆成 4 段，每段一个 goroutine 并行计算，最后汇总。在多核机器上，你能看到明显的加速。</p><h3 id="11-3-并发执行多个独立任务">11.3 并发执行多个独立任务</h3><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></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;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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 模拟多个独立的初始化任务</span></span><br><span class="line">    tasks := <span class="keyword">map</span>[<span class="type">string</span>]time.Duration&#123;</span><br><span class="line">        <span class="string">&quot;加载配置&quot;</span>:   <span class="number">200</span> * time.Millisecond,</span><br><span class="line">        <span class="string">&quot;连接数据库&quot;</span>:  <span class="number">300</span> * time.Millisecond,</span><br><span class="line">        <span class="string">&quot;初始化缓存&quot;</span>:  <span class="number">150</span> * time.Millisecond,</span><br><span class="line">        <span class="string">&quot;加载模板&quot;</span>:   <span class="number">100</span> * time.Millisecond,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    start := time.Now()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> name, duration := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(taskName <span class="type">string</span>, taskDuration time.Duration)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            fmt.Printf(<span class="string">&quot;[开始] %s\n&quot;</span>, taskName)</span><br><span class="line">            time.Sleep(taskDuration)</span><br><span class="line">            fmt.Printf(<span class="string">&quot;[完成] %s (耗时 %v)\n&quot;</span>, taskName, taskDuration)</span><br><span class="line">        &#125;(name, duration)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n所有初始化完成，总耗时: %v\n&quot;</span>, time.Since(start))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可能的输出：</p><figure class="highlight scss"><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="selector-attr">[开始]</span> 加载模板</span><br><span class="line"><span class="selector-attr">[开始]</span> 加载配置</span><br><span class="line"><span class="selector-attr">[开始]</span> 连接数据库</span><br><span class="line"><span class="selector-attr">[开始]</span> 初始化缓存</span><br><span class="line"><span class="selector-attr">[完成]</span> 加载模板 (耗时 <span class="number">100ms</span>)</span><br><span class="line"><span class="selector-attr">[完成]</span> 初始化缓存 (耗时 <span class="number">150ms</span>)</span><br><span class="line"><span class="selector-attr">[完成]</span> 加载配置 (耗时 <span class="number">200ms</span>)</span><br><span class="line"><span class="selector-attr">[完成]</span> 连接数据库 (耗时 <span class="number">300ms</span>)</span><br><span class="line"></span><br><span class="line">所有初始化完成，总耗时: <span class="number">301ms</span></span><br></pre></td></tr></table></figure><p>四个任务如果顺序执行需要 750ms，并发执行只需要 300ms（取决于最慢的任务）。这在 Web 服务的启动阶段很常见——多个初始化任务互不依赖，可以并行完成。</p><hr><h2 id="12-常见坑总结">12. 常见坑总结</h2><h3 id="12-1-忘了等-goroutine-完成">12.1 忘了等 goroutine 完成</h3><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="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">go</span> doSomething()</span><br><span class="line">    <span class="comment">// main 直接返回，goroutine 来不及执行</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>解决</strong>：用 <code>sync.WaitGroup</code> 等待。</p><h3 id="12-2-WaitGroup-传值而不是传指针">12.2 WaitGroup 传值而不是传指针</h3><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"><span class="comment">// 错误</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(wg sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()  <span class="comment">// 操作的是副本！</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">worker</span><span class="params">(wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-3-Add-和-go-的顺序">12.3 Add 和 go 的顺序</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误：可能在 Add 之前 Wait 就执行了</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</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">        wg.Add(<span class="number">1</span>)  <span class="comment">// 在 goroutine 内部 Add</span></span><br><span class="line">        <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br><span class="line">wg.Wait()  <span class="comment">// 可能在任何 goroutine 执行 Add 之前就到这了</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：先 Add，再 go</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">    wg.Add(<span class="number">1</span>)  <span class="comment">// 在主 goroutine 中 Add</span></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">        <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br><span class="line">wg.Wait()</span><br></pre></td></tr></table></figure><p><code>wg.Add(1)</code> 必须在 <code>go</code> 之前调用。如果在 goroutine 内部调用，可能还没来得及 <code>Add</code>，主 goroutine 就已经 <code>Wait</code> 了（此时计数器为 0，<code>Wait</code> 直接返回）。</p><h3 id="12-4-闭包捕获循环变量">12.4 闭包捕获循环变量</h3><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="comment">// 经典 bug</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</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 class="comment">// 全部输出 3</span></span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(id <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">        fmt.Println(id)  <span class="comment">// 输出 0、1、2（顺序不定）</span></span><br><span class="line">    &#125;(i)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="12-5-在-goroutine-里-panic">12.5 在 goroutine 里 panic</h3><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="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">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="built_in">panic</span>(<span class="string">&quot;goroutine 挂了&quot;</span>)  <span class="comment">// 整个程序崩溃！</span></span><br><span class="line">    &#125;()</span><br><span class="line">    time.Sleep(time.Second)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>goroutine 里的 panic 会导致整个程序崩溃</strong>，不仅仅是那一个 goroutine。如果需要防止这种情况，要在 goroutine 内部自己 recover（第 17 课）：</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"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</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">        <span class="keyword">if</span> r := <span class="built_in">recover</span>(); r != <span class="literal">nil</span> &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;goroutine 恢复: %v\n&quot;</span>, r)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="built_in">panic</span>(<span class="string">&quot;出错了&quot;</span>)</span><br><span class="line">&#125;()</span><br></pre></td></tr></table></figure><h3 id="12-6-goroutine-泄漏">12.6 goroutine 泄漏</h3><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">leak</span><span class="params">()</span></span> &#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">        <span class="keyword">for</span> &#123;</span><br><span class="line">            <span class="comment">// 永远不会结束的循环</span></span><br><span class="line">            time.Sleep(time.Second)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="comment">// 函数返回了，但 goroutine 还在后台运行</span></span><br><span class="line">    <span class="comment">// 没有任何方式让它停下来</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果不断调用 <code>leak()</code>，每次都会创建一个永远不会结束的 goroutine，内存会持续增长。这就是 <strong>goroutine 泄漏</strong>。后面第 29 课会讲用 <code>context</code> 来控制 goroutine 的生命周期。</p><hr><h2 id="13-本课练习">13. 本课练习</h2><h3 id="练习-1：并发计时器">练习 1：并发计时器</h3><p>要求：</p><ul><li>启动 3 个 goroutine</li><li>每个 goroutine 每隔 500ms 打印一次自己的名字和当前时间</li><li>每个 goroutine 打印 5 次后结束</li><li>主 goroutine 等所有 goroutine 完成后打印&quot;全部完成&quot;</li></ul><hr><h3 id="练习-2：并发求和">练习 2：并发求和</h3><p>要求：</p><ul><li>给定一个有 100 个元素的 int 切片</li><li>把它分成 10 段，每段 10 个元素</li><li>启动 10 个 goroutine 分别计算每段的和</li><li>主 goroutine 汇总结果并输出总和</li></ul><hr><h3 id="练习-3：并发文件处理">练习 3：并发文件处理</h3><p>要求：</p><ul><li>创建 5 个文本文件（<code>file1.txt</code> ~ <code>file5.txt</code>），每个文件写入一些文本</li><li>启动 5 个 goroutine 同时读取这 5 个文件</li><li>每个 goroutine 统计文件中的字符数</li><li>主 goroutine 汇总并输出总字符数</li></ul><p>提示：用第 19 课学的文件操作配合 goroutine。</p><hr><h3 id="练习-4：感受闭包陷阱">练习 4：感受闭包陷阱</h3><p>要求：</p><ul><li>写一段代码，故意触发闭包捕获循环变量的 bug，观察输出</li><li>然后用两种方式修复它（传参数、创建局部变量），对比输出</li></ul><hr><h3 id="练习-5：goroutine-数量实验">练习 5：goroutine 数量实验</h3><p>要求：</p><ul><li>写一个程序，分别创建 100、1000、10000、100000 个 goroutine</li><li>每个 goroutine 做一个简单操作（比如把数字加到结果中）</li><li>记录并打印每种数量下的总耗时</li><li>观察 goroutine 数量和耗时的关系</li></ul><hr><h2 id="14-自测题">14. 自测题</h2><h3 id="14-1-概念题">14.1 概念题</h3><ol><li><code>go</code> 关键字的作用是什么？</li><li>主 goroutine 退出后，其他 goroutine 会怎样？</li><li><code>sync.WaitGroup</code> 的三个方法分别是什么？各在什么时候调用？</li><li>为什么 <code>WaitGroup</code> 作为函数参数时必须传指针？</li><li>为什么 <code>wg.Add(1)</code> 必须在 <code>go</code> 语句之前而不是在 goroutine 内部？</li><li>goroutine 和操作系统线程有什么区别？</li><li>什么是闭包变量捕获问题？怎么解决？</li><li>goroutine 中发生 panic 会怎样？</li></ol><h3 id="14-2-代码阅读题">14.2 代码阅读题</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><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></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;sync&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="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">3</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> &#123;</span><br><span class="line">            <span class="keyword">defer</span> wg.Done()</span><br><span class="line">            fmt.Printf(<span class="string">&quot;%d &quot;</span>, n*n)</span><br><span class="line">        &#125;(i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">    fmt.Println(<span class="string">&quot;done&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">X</span> <span class="attribute">X</span> <span class="attribute">X</span> done</span><br></pre></td></tr></table></figure><p>其中 X 是 0、1、4 的某种排列（比如 <code>4 0 1 done</code> 或 <code>0 1 4 done</code>）。</p><p>解释：</p><ol><li>循环启动 3 个 goroutine，分别传入 i=0、1、2</li><li>每个 goroutine 计算 n*n 并打印：0*0=0、1*1=1、2*2=4</li><li>三个 goroutine 的<strong>执行顺序不确定</strong>，所以 0、1、4 可能以任意顺序出现</li><li><code>wg.Wait()</code> 保证三个 goroutine 都完成后才打印 “done”</li><li>“done” 一定是最后输出的</li></ol></details><hr><h2 id="15-本课总结">15. 本课总结</h2><p>这一课你学到了 Go 并发编程的基础。</p><table><thead><tr><th>场景</th><th>做法</th></tr></thead><tbody><tr><td>创建 goroutine</td><td>函数调用前加 <code>go</code></td></tr><tr><td>等待 goroutine 完成</td><td><code>sync.WaitGroup</code></td></tr><tr><td>goroutine 中用循环变量</td><td>作为参数传入，避免闭包陷阱</td></tr><tr><td>goroutine 中防 panic</td><td>在 goroutine 内部 <code>defer recover</code></td></tr><tr><td>获取 goroutine 的结果</td><td>预分配结果切片，按索引写入</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong><code>go</code> 关键字创建 goroutine，但主 goroutine 退出就全完了——必须用 WaitGroup 等待</strong></li><li><strong>goroutine 的执行顺序不确定——不要依赖顺序，要用同步机制协调</strong></li><li><strong>闭包捕获循环变量是经典 bug——永远把循环变量作为参数传给 goroutine</strong></li></ol><hr><h2 id="16-下一课预告">16. 下一课预告</h2><p>这一课你学会了启动多个 goroutine 并行工作。但你有没有发现一个问题：<strong>goroutine 之间怎么通信？</strong></p><p>目前我们只能用共享变量传递数据，而且还很容易出问题。Go 有一句著名的格言：</p><blockquote><p><strong>“不要通过共享内存来通信，而要通过通信来共享内存。”</strong></p></blockquote><p>下一课：<strong>channel 入门</strong></p><p>会重点讲：</p><ul><li>channel 是什么——goroutine 之间的通信管道</li><li>无缓冲 channel 和有缓冲 channel 的区别</li><li>怎么用 channel 发送和接收数据</li><li>channel 的阻塞行为</li><li>用 <code>range</code> 遍历 channel</li><li>关闭 channel</li></ul><p>学完下一课，你就能让 goroutine 之间安全、优雅地互相传递数据了。</p>]]></content>
    
    
    <summary type="html">Go语言系列25</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 24 课：命令行小项目实战</title>
    <link href="https://yjyrichard.github.io/posts/805b716d.html"/>
    <id>https://yjyrichard.github.io/posts/805b716d.html</id>
    <published>2026-03-22T15:28:11.712Z</published>
    <updated>2026-03-22T15:40:40.006Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 24 课：命令行小项目实战</h1><blockquote><p>学习定位：这是整套 Go 教程的第 24 课，也是阶段四（标准库与实战阶段）的最后一课。<br>前置要求：已经完成第 23 课，掌握了排序与集合操作。需要综合运用前面所有课程的知识。<br>本课目标：整合前面所学的结构体、切片、map、文件操作、JSON、排序、错误处理、时间处理、字符串处理等知识，从零完成一个具有实际功能的命令行待办事项管理器。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>前面 23 课你学了很多东西：变量、函数、结构体、切片、map、接口、文件、JSON、排序……但它们都是零散的。这一课要做的事就一个：<strong>把它们串起来，写一个真正能用的程序</strong>。</p><p>我们要完成一个<strong>命令行待办事项管理器</strong>（Todo Manager），它能做到：</p><ul><li>添加待办事项（标题、分类、优先级、截止日期）</li><li>列出所有待办事项（支持多种排序方式）</li><li>标记任务为已完成</li><li>删除任务</li><li>按关键字搜索</li><li>按分类筛选</li><li>显示统计信息</li><li>数据保存到文件（关闭程序后不丢失）</li></ul><p>这个项目会用到你前面学的几乎所有东西：</p><table><thead><tr><th>知识点</th><th>对应课程</th><th>在项目中的用途</th></tr></thead><tbody><tr><td>输入输出</td><td>第 3 课</td><td>命令行交互</td></tr><tr><td>条件判断与循环</td><td>第 4、5 课</td><td>菜单逻辑、遍历</td></tr><tr><td>函数</td><td>第 6 课</td><td>功能拆分</td></tr><tr><td>指针</td><td>第 7 课</td><td>修改任务状态</td></tr><tr><td>切片</td><td>第 9 课</td><td>存储任务列表</td></tr><tr><td>map</td><td>第 10 课</td><td>分类统计</td></tr><tr><td>字符串</td><td>第 11、21 课</td><td>搜索匹配</td></tr><tr><td>结构体</td><td>第 12 课</td><td>任务数据模型</td></tr><tr><td>方法</td><td>第 13 课</td><td>任务行为</td></tr><tr><td>错误处理</td><td>第 16 课</td><td>文件读写、输入校验</td></tr><tr><td>defer</td><td>第 17 课</td><td>文件关闭</td></tr><tr><td>文件操作</td><td>第 19 课</td><td>数据持久化</td></tr><tr><td>时间处理</td><td>第 20 课</td><td>截止日期、创建时间</td></tr><tr><td>JSON</td><td>第 22 课</td><td>数据序列化</td></tr><tr><td>排序</td><td>第 23 课</td><td>任务排序</td></tr></tbody></table><p>学完这一课，你就完成了整个阶段四，具备了用 Go 做真实小项目的完整能力。</p><hr><h2 id="2-项目设计——动手之前先想清楚">2. 项目设计——动手之前先想清楚</h2><h3 id="2-1-需求拆解">2.1 需求拆解</h3><p>一个待办事项管理器，最核心的事情是什么？<strong>管理任务</strong>。一个任务需要哪些信息？</p><figure class="highlight abnf"><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">任务 <span class="operator">=</span> &#123;</span><br><span class="line">    编号（唯一标识）</span><br><span class="line">    标题（做什么事）</span><br><span class="line">    分类（工作？学习？生活？）</span><br><span class="line">    优先级（高/中/低）</span><br><span class="line">    是否完成</span><br><span class="line">    创建时间</span><br><span class="line">    截止日期（可选）</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用户通过命令行菜单操作这些任务，所有数据保存到一个 JSON 文件里。</p><h3 id="2-2-交互设计">2.2 交互设计</h3><p>程序启动后显示菜单，用户输入数字选择操作：</p><figure class="highlight abnf"><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="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span> 待办事项管理器 <span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span></span><br><span class="line">  <span class="number">1</span>. 添加任务</span><br><span class="line">  <span class="number">2</span>. 查看所有任务</span><br><span class="line">  <span class="number">3</span>. 标记任务完成</span><br><span class="line">  <span class="number">4</span>. 删除任务</span><br><span class="line">  <span class="number">5</span>. 搜索任务</span><br><span class="line">  <span class="number">6</span>. 按分类查看</span><br><span class="line">  <span class="number">7</span>. 统计信息</span><br><span class="line">  <span class="number">0</span>. 保存并退出</span><br><span class="line"><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span><span class="operator">=</span></span><br><span class="line">请选择操作：</span><br></pre></td></tr></table></figure><h3 id="2-3-数据存储">2.3 数据存储</h3><p>用 JSON 文件存储，格式大概长这样：</p><figure class="highlight json"><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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;next_id&quot;</span><span class="punctuation">:</span> <span class="number">4</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;学完 Go 第 24 课&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;category&quot;</span><span class="punctuation">:</span> <span class="string">&quot;学习&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;priority&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;done&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;created_at&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2025-01-15T10:30:00+08:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;due_date&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2025-01-20T23:59:59+08:00&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>设计想清楚了，开始写代码。</p><hr><h2 id="3-第一步：定义数据结构">3. 第一步：定义数据结构</h2><h3 id="3-1-任务结构体">3.1 任务结构体</h3><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></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;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 优先级常量</span></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    PriorityHigh   = <span class="number">1</span></span><br><span class="line">    PriorityMedium = <span class="number">2</span></span><br><span class="line">    PriorityLow    = <span class="number">3</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Task 表示一个待办事项</span></span><br><span class="line"><span class="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID        <span class="type">int</span>       <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Title     <span class="type">string</span>    <span class="string">`json:&quot;title&quot;`</span></span><br><span class="line">    Category  <span class="type">string</span>    <span class="string">`json:&quot;category&quot;`</span></span><br><span class="line">    Priority  <span class="type">int</span>       <span class="string">`json:&quot;priority&quot;`</span></span><br><span class="line">    Done      <span class="type">bool</span>      <span class="string">`json:&quot;done&quot;`</span></span><br><span class="line">    CreatedAt time.Time <span class="string">`json:&quot;created_at&quot;`</span></span><br><span class="line">    DueDate   time.Time <span class="string">`json:&quot;due_date,omitempty&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>设计说明</strong>：</p><ul><li><code>ID</code> 是唯一标识，方便用户指定&quot;完成第几号任务&quot;</li><li><code>Priority</code> 用 int 表示（1=高、2=中、3=低），数字越小优先级越高，方便排序</li><li><code>DueDate</code> 加了 <code>omitempty</code>，没设截止日期时 JSON 里不输出这个字段</li><li>JSON tag 用蛇形命名，这是 JSON 的常见风格（第 22 课）</li></ul><h3 id="3-2-优先级的显示方法">3.2 优先级的显示方法</h3><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="comment">// PriorityText 返回优先级的中文描述</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">PriorityText</span><span class="params">(p <span class="type">int</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> p &#123;</span><br><span class="line">    <span class="keyword">case</span> PriorityHigh:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;高&quot;</span></span><br><span class="line">    <span class="keyword">case</span> PriorityMedium:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;中&quot;</span></span><br><span class="line">    <span class="keyword">case</span> PriorityLow:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;低&quot;</span></span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;未知&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-3-任务的显示方法">3.3 任务的显示方法</h3><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></pre></td><td class="code"><pre><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="comment">// String 返回任务的格式化字符串</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t Task)</span></span> String() <span class="type">string</span> &#123;</span><br><span class="line">    status := <span class="string">&quot;[ ]&quot;</span></span><br><span class="line">    <span class="keyword">if</span> t.Done &#123;</span><br><span class="line">        status = <span class="string">&quot;[✓]&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    due := <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">if</span> !t.DueDate.IsZero() &#123;</span><br><span class="line">        due = fmt.Sprintf(<span class="string">&quot; | 截止: %s&quot;</span>, t.DueDate.Format(<span class="string">&quot;2006-01-02&quot;</span>))</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 如果未完成且已过期，标注提醒</span></span><br><span class="line">        <span class="keyword">if</span> !t.Done &amp;&amp; time.Now().After(t.DueDate) &#123;</span><br><span class="line">            due += <span class="string">&quot; (已过期!)&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;%s #%d [%s] %s (%s)%s&quot;</span>,</span><br><span class="line">        status, t.ID, PriorityText(t.Priority), t.Title, t.Category, due)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里用到了：</p><ul><li><strong>方法</strong>（第 13 课）：给 <code>Task</code> 定义 <code>String()</code> 方法</li><li><strong>时间格式化</strong>（第 20 课）：<code>t.DueDate.Format(&quot;2006-01-02&quot;)</code></li><li><strong>时间比较</strong>（第 20 课）：<code>time.Now().After(t.DueDate)</code> 判断是否过期</li><li><strong>零值判断</strong>：<code>t.DueDate.IsZero()</code> 判断有没有设置截止日期</li></ul><h3 id="3-4-数据存储结构">3.4 数据存储结构</h3><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="comment">// TodoList 是整个待办事项列表，也是 JSON 文件的顶层结构</span></span><br><span class="line"><span class="keyword">type</span> TodoList <span class="keyword">struct</span> &#123;</span><br><span class="line">    NextID <span class="type">int</span>    <span class="string">`json:&quot;next_id&quot;`</span></span><br><span class="line">    Tasks  []Task <span class="string">`json:&quot;tasks&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// NewTodoList 创建一个空的待办列表</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewTodoList</span><span class="params">()</span></span> *TodoList &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;TodoList&#123;</span><br><span class="line">        NextID: <span class="number">1</span>,</span><br><span class="line">        Tasks:  []Task&#123;&#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>NextID</code> 记录下一个可用的 ID。每次添加任务就用这个 ID，然后自增。这样即使删除了任务，ID 也不会重复。</p><hr><h2 id="4-第二步：实现核心功能">4. 第二步：实现核心功能</h2><h3 id="4-1-添加任务">4.1 添加任务</h3><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="comment">// AddTask 添加一个新任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> AddTask(title, category <span class="type">string</span>, priority <span class="type">int</span>, dueDate time.Time) &#123;</span><br><span class="line">    task := Task&#123;</span><br><span class="line">        ID:        tl.NextID,</span><br><span class="line">        Title:     title,</span><br><span class="line">        Category:  category,</span><br><span class="line">        Priority:  priority,</span><br><span class="line">        Done:      <span class="literal">false</span>,</span><br><span class="line">        CreatedAt: time.Now(),</span><br><span class="line">        DueDate:   dueDate,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tl.Tasks = <span class="built_in">append</span>(tl.Tasks, task)</span><br><span class="line">    tl.NextID++</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么用指针接收者</strong>（第 13 课）：<code>AddTask</code> 要修改 <code>TodoList</code> 的内容（添加元素、递增 ID），如果用值接收者，修改的是副本，原始数据不会变。</p><h3 id="4-2-标记完成">4.2 标记完成</h3><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">import</span> <span class="string">&quot;errors&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// CompleteTask 将指定 ID 的任务标记为已完成</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> CompleteTask(id <span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].ID == id &#123;</span><br><span class="line">            <span class="keyword">if</span> tl.Tasks[i].Done &#123;</span><br><span class="line">                <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;任务 #%d 已经完成过了&quot;</span>, id)</span><br><span class="line">            &#125;</span><br><span class="line">            tl.Tasks[i].Done = <span class="literal">true</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;找不到任务 #%d&quot;</span>, id)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>注意 <code>for i := range</code> 而不是 <code>for _, task := range</code></strong>。</p><p>这是第 9 课和第 12 课提过的经典问题：<code>for _, task := range</code> 中的 <code>task</code> 是副本，修改 <code>task.Done</code> 不会影响切片中的原始数据。用索引 <code>tl.Tasks[i].Done = true</code> 才能真正修改。</p><p><strong>错误处理</strong>（第 16 课）：找不到任务或任务已完成，都返回错误信息。</p><h3 id="4-3-删除任务">4.3 删除任务</h3><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="comment">// DeleteTask 删除指定 ID 的任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> DeleteTask(id <span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.ID == id &#123;</span><br><span class="line">            <span class="comment">// 用切片技巧删除元素（第 9 课）</span></span><br><span class="line">            tl.Tasks = <span class="built_in">append</span>(tl.Tasks[:i], tl.Tasks[i+<span class="number">1</span>:]...)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;找不到任务 #%d&quot;</span>, id)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>切片删除元素的技巧，第 9 课讲过：<code>append(s[:i], s[i+1:]...)</code>。</p><h3 id="4-4-搜索任务">4.4 搜索任务</h3><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">import</span> <span class="string">&quot;strings&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// SearchTasks 按关键字搜索任务（匹配标题和分类）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> SearchTasks(keyword <span class="type">string</span>) []Task &#123;</span><br><span class="line">    keyword = strings.ToLower(keyword)</span><br><span class="line">    result := []Task&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> strings.Contains(strings.ToLower(task.Title), keyword) ||</span><br><span class="line">            strings.Contains(strings.ToLower(task.Category), keyword) &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>字符串处理</strong>（第 21 课）：<code>strings.ToLower</code> 转小写实现大小写不敏感搜索，<code>strings.Contains</code> 判断是否包含子串。</p><h3 id="4-5-按分类筛选">4.5 按分类筛选</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// GetByCategory 返回指定分类的所有任务</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> GetByCategory(category <span class="type">string</span>) []Task &#123;</span><br><span class="line">    result := []Task&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.Category == category &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetCategories 返回所有分类名称（去重）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> GetCategories() []<span class="type">string</span> &#123;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    categories := []<span class="type">string</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[task.Category] &#123;</span><br><span class="line">            seen[task.Category] = <span class="literal">true</span></span><br><span class="line">            categories = <span class="built_in">append</span>(categories, task.Category)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> categories</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>去重用到了第 23 课的 map 去重技巧。</p><h3 id="4-6-排序功能">4.6 排序功能</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&quot;sort&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// SortByPriority 按优先级排序（高优先级在前）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> SortByPriority() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="comment">// 未完成的排在已完成的前面</span></span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].Done != tl.Tasks[j].Done &#123;</span><br><span class="line">            <span class="keyword">return</span> !tl.Tasks[i].Done</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 同状态按优先级排序（数字小 = 优先级高）</span></span><br><span class="line">        <span class="keyword">return</span> tl.Tasks[i].Priority &lt; tl.Tasks[j].Priority</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// SortByDueDate 按截止日期排序（最紧急的在前）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> SortByDueDate() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="comment">// 未完成的排在已完成的前面</span></span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].Done != tl.Tasks[j].Done &#123;</span><br><span class="line">            <span class="keyword">return</span> !tl.Tasks[i].Done</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 有截止日期的排在没有的前面</span></span><br><span class="line">        iHasDue := !tl.Tasks[i].DueDate.IsZero()</span><br><span class="line">        jHasDue := !tl.Tasks[j].DueDate.IsZero()</span><br><span class="line">        <span class="keyword">if</span> iHasDue != jHasDue &#123;</span><br><span class="line">            <span class="keyword">return</span> iHasDue</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 都有截止日期，早的排前面</span></span><br><span class="line">        <span class="keyword">if</span> iHasDue &amp;&amp; jHasDue &#123;</span><br><span class="line">            <span class="keyword">return</span> tl.Tasks[i].DueDate.Before(tl.Tasks[j].DueDate)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// SortByCreatedAt 按创建时间排序（最新的在前）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> SortByCreatedAt() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> tl.Tasks[i].CreatedAt.After(tl.Tasks[j].CreatedAt)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么用 <code>SliceStable</code></strong>（第 23 课）：稳定排序保证同优先级的任务保持原来的顺序。</p><p><strong>多字段排序</strong>（第 23 课）：先按完成状态分组，再按优先级排序。这正是第 23 课讲的多字段排序技巧。</p><h3 id="4-7-统计信息">4.7 统计信息</h3><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="comment">// Stats 返回任务的统计信息</span></span><br><span class="line"><span class="keyword">type</span> Stats <span class="keyword">struct</span> &#123;</span><br><span class="line">    Total     <span class="type">int</span></span><br><span class="line">    Done      <span class="type">int</span></span><br><span class="line">    Pending   <span class="type">int</span></span><br><span class="line">    Overdue   <span class="type">int</span></span><br><span class="line">    ByCategory <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span></span><br><span class="line">    ByPriority <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</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">(tl *TodoList)</span></span> GetStats() Stats &#123;</span><br><span class="line">    stats := Stats&#123;</span><br><span class="line">        Total:      <span class="built_in">len</span>(tl.Tasks),</span><br><span class="line">        ByCategory: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>),</span><br><span class="line">        ByPriority: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>),</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    now := time.Now()</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.Done &#123;</span><br><span class="line">            stats.Done++</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            stats.Pending++</span><br><span class="line">            <span class="keyword">if</span> !task.DueDate.IsZero() &amp;&amp; now.After(task.DueDate) &#123;</span><br><span class="line">                stats.Overdue++</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        stats.ByCategory[task.Category]++</span><br><span class="line">        stats.ByPriority[PriorityText(task.Priority)]++</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> stats</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>map 统计</strong>（第 10 课）：用 <code>map[string]int</code> 按分类和优先级计数。</p><hr><h2 id="5-第三步：数据持久化">5. 第三步：数据持久化</h2><h3 id="5-1-保存到文件">5.1 保存到文件</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;encoding/json&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> dataFile = <span class="string">&quot;todos.json&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Save 将数据保存到 JSON 文件</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> Save() <span class="type">error</span> &#123;</span><br><span class="line">    data, err := json.MarshalIndent(tl, <span class="string">&quot;&quot;</span>, <span class="string">&quot;  &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;序列化失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    err = os.WriteFile(dataFile, data, <span class="number">0644</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;写入文件失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>JSON 编码</strong>（第 22 课）：<code>json.MarshalIndent</code> 生成格式化的 JSON，方便人类阅读。</p><p><strong>文件写入</strong>（第 19 课）：<code>os.WriteFile</code> 直接写入整个文件。</p><p><strong>错误包装</strong>（第 16 课）：用 <code>%w</code> 包装原始错误，保留错误链。</p><h3 id="5-2-从文件加载">5.2 从文件加载</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Load 从 JSON 文件加载数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Load</span><span class="params">()</span></span> (*TodoList, <span class="type">error</span>) &#123;</span><br><span class="line">    data, err := os.ReadFile(dataFile)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> os.IsNotExist(err) &#123;</span><br><span class="line">            <span class="comment">// 文件不存在，返回空列表（首次使用）</span></span><br><span class="line">            <span class="keyword">return</span> NewTodoList(), <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;读取文件失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> tl TodoList</span><br><span class="line">    err = json.Unmarshal(data, &amp;tl)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;解析 JSON 失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> &amp;tl, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>文件不存在的处理</strong>：第一次运行程序时 <code>todos.json</code> 不存在，这是正常情况，不应该报错，而应该返回一个空列表。<code>os.IsNotExist(err)</code> 判断是否是&quot;文件不存在&quot;的错误。</p><p><strong>JSON 解码</strong>（第 22 课）：<code>json.Unmarshal</code> 把 JSON 数据解析到结构体中。</p><hr><h2 id="6-第四步：命令行交互">6. 第四步：命令行交互</h2><h3 id="6-1-主菜单">6.1 主菜单</h3><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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&quot;bufio&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="comment">// 加载数据</span></span><br><span class="line">    todoList, err := Load()</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;加载数据失败: %v\n&quot;</span>, err)</span><br><span class="line">        fmt.Println(<span class="string">&quot;将使用空列表启动&quot;</span>)</span><br><span class="line">        todoList = NewTodoList()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    scanner := bufio.NewScanner(os.Stdin)</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;欢迎使用待办事项管理器！&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        printMenu()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">        choice := strings.TrimSpace(scanner.Text())</span><br><span class="line"></span><br><span class="line">        <span class="keyword">switch</span> choice &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;1&quot;</span>:</span><br><span class="line">            handleAdd(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;2&quot;</span>:</span><br><span class="line">            handleList(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;3&quot;</span>:</span><br><span class="line">            handleComplete(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;4&quot;</span>:</span><br><span class="line">            handleDelete(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;5&quot;</span>:</span><br><span class="line">            handleSearch(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;6&quot;</span>:</span><br><span class="line">            handleCategoryView(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;7&quot;</span>:</span><br><span class="line">            handleStats(todoList)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;0&quot;</span>:</span><br><span class="line">            err := todoList.Save()</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;保存失败: %v\n&quot;</span>, err)</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                fmt.Println(<span class="string">&quot;数据已保存，再见！&quot;</span>)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Println(<span class="string">&quot;无效的选项，请重新选择&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</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">printMenu</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println()</span><br><span class="line">    fmt.Println(<span class="string">&quot;========== 待办事项管理器 ==========&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  1. 添加任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  2. 查看所有任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  3. 标记任务完成&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  4. 删除任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  5. 搜索任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  6. 按分类查看&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  7. 统计信息&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  0. 保存并退出&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;====================================&quot;</span>)</span><br><span class="line">    fmt.Print(<span class="string">&quot;请选择操作: &quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么用 <code>bufio.Scanner</code> 而不是 <code>fmt.Scan</code></strong>：<code>fmt.Scan</code> 遇到空格就停了，读不了一整行。<code>bufio.Scanner</code> 按行读取，能处理带空格的输入（比如任务标题&quot;写 Go 作业&quot;）。</p><h3 id="6-2-添加任务的交互">6.2 添加任务的交互</h3><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><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="string">&quot;strconv&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleAdd</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 添加任务 ---&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读取标题</span></span><br><span class="line">    fmt.Print(<span class="string">&quot;任务标题: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    title := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> title == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;标题不能为空&quot;</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="comment">// 读取分类</span></span><br><span class="line">    fmt.Print(<span class="string">&quot;分类 (如: 工作/学习/生活，默认: 其他): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    category := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> category == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        category = <span class="string">&quot;其他&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读取优先级</span></span><br><span class="line">    fmt.Print(<span class="string">&quot;优先级 (1=高 2=中 3=低，默认: 2): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    priorityStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    priority := PriorityMedium <span class="comment">// 默认中等</span></span><br><span class="line">    <span class="keyword">if</span> priorityStr != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        p, err := strconv.Atoi(priorityStr)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> || p &lt; <span class="number">1</span> || p &gt; <span class="number">3</span> &#123;</span><br><span class="line">            fmt.Println(<span class="string">&quot;优先级无效，使用默认值(中)&quot;</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            priority = p</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读取截止日期</span></span><br><span class="line">    fmt.Print(<span class="string">&quot;截止日期 (格式: 2006-01-02，留空跳过): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    dueDateStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">var</span> dueDate time.Time</span><br><span class="line">    <span class="keyword">if</span> dueDateStr != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        parsed, err := time.Parse(<span class="string">&quot;2006-01-02&quot;</span>, dueDateStr)</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;日期格式不对，跳过截止日期&quot;</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 设置为当天的 23:59:59</span></span><br><span class="line">            dueDate = parsed.Add(<span class="number">23</span>*time.Hour + <span class="number">59</span>*time.Minute + <span class="number">59</span>*time.Second)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tl.AddTask(title, category, priority, dueDate)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;任务已添加: #%d %s\n&quot;</span>, tl.NextID<span class="number">-1</span>, title)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个函数展示了<strong>完整的用户输入处理流程</strong>：</p><ol><li>提示用户输入</li><li>读取一行</li><li>去除空白（<code>strings.TrimSpace</code>，第 21 课）</li><li>校验输入（空值检查）</li><li>类型转换（<code>strconv.Atoi</code>，第 21 课）</li><li>提供默认值（分类默认&quot;其他&quot;，优先级默认&quot;中&quot;）</li><li>解析时间（<code>time.Parse</code>，第 20 课）</li></ol><h3 id="6-3-查看任务列表">6.3 查看任务列表</h3><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="function"><span class="keyword">func</span> <span class="title">handleList</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(tl.Tasks) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n排序方式:&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  1. 按优先级&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  2. 按截止日期&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  3. 按创建时间&quot;</span>)</span><br><span class="line">    fmt.Print(<span class="string">&quot;选择 (默认: 1): &quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    sortChoice := strings.TrimSpace(scanner.Text())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">switch</span> sortChoice &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;2&quot;</span>:</span><br><span class="line">        tl.SortByDueDate()</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;3&quot;</span>:</span><br><span class="line">        tl.SortByCreatedAt()</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        tl.SortByPriority()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 任务列表 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        fmt.Println(task) <span class="comment">// 自动调用 task.String()</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n共 %d 个任务\n&quot;</span>, <span class="built_in">len</span>(tl.Tasks))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong><code>fmt.Println(task)</code> 自动调用 <code>String()</code> 方法</strong>：这是 Go 的约定（第 13 课提到的 <code>Stringer</code> 接口）。只要类型实现了 <code>String() string</code> 方法，<code>fmt.Println</code> 就会用它来格式化输出。</p><h3 id="6-4-标记完成">6.4 标记完成</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleComplete</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 先显示未完成的任务</span></span><br><span class="line">    pending := []Task&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> !task.Done &#123;</span><br><span class="line">            pending = <span class="built_in">append</span>(pending, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(pending) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有未完成的任务&quot;</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">    fmt.Println(<span class="string">&quot;\n--- 未完成的任务 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> pending &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入要完成的任务编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    id, err := strconv.Atoi(idStr)</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;请输入有效的数字&quot;</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">    err = tl.CompleteTask(id)</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;操作失败: %v\n&quot;</span>, err)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;任务 #%d 已标记为完成!\n&quot;</span>, id)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-5-删除任务">6.5 删除任务</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleDelete</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(tl.Tasks) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n--- 所有任务 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入要删除的任务编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    id, err := strconv.Atoi(idStr)</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;请输入有效的数字&quot;</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="comment">// 二次确认</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;确认删除任务 #%d? (y/n): &quot;</span>, id)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    confirm := strings.TrimSpace(strings.ToLower(scanner.Text()))</span><br><span class="line">    <span class="keyword">if</span> confirm != <span class="string">&quot;y&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;已取消删除&quot;</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">    err = tl.DeleteTask(id)</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;删除失败: %v\n&quot;</span>, err)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;任务 #%d 已删除\n&quot;</span>, id)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>二次确认是好习惯</strong>：删除是不可逆的操作，加一步确认可以防止误操作。</p><h3 id="6-6-搜索任务">6.6 搜索任务</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleSearch</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入搜索关键字: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    keyword := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> keyword == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;关键字不能为空&quot;</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">    results := tl.SearchTasks(keyword)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(results) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;没有找到包含 \&quot;%s\&quot; 的任务\n&quot;</span>, keyword)</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">    fmt.Printf(<span class="string">&quot;\n--- 搜索结果 (%d 条) ---\n&quot;</span>, <span class="built_in">len</span>(results))</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> results &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-7-按分类查看">6.7 按分类查看</h3><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="function"><span class="keyword">func</span> <span class="title">handleCategoryView</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    categories := tl.GetCategories()</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(categories) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n可用分类:&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i, cat := <span class="keyword">range</span> categories &#123;</span><br><span class="line">        count := <span class="built_in">len</span>(tl.GetByCategory(cat))</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %d. %s (%d 个任务)\n&quot;</span>, i+<span class="number">1</span>, cat, count)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n选择分类编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idxStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    idx, err := strconv.Atoi(idxStr)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> || idx &lt; <span class="number">1</span> || idx &gt; <span class="built_in">len</span>(categories) &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;无效的选择&quot;</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">    category := categories[idx<span class="number">-1</span>]</span><br><span class="line">    tasks := tl.GetByCategory(category)</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n--- 分类: %s ---\n&quot;</span>, category)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-8-统计信息">6.8 统计信息</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleStats</span><span class="params">(tl *TodoList)</span></span> &#123;</span><br><span class="line">    stats := tl.GetStats()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 统计信息 ---&quot;</span>)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;总任务数: %d\n&quot;</span>, stats.Total)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> stats.Total == <span class="number">0</span> &#123;</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">    fmt.Printf(<span class="string">&quot;已完成:   %d\n&quot;</span>, stats.Done)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;未完成:   %d\n&quot;</span>, stats.Pending)</span><br><span class="line">    <span class="keyword">if</span> stats.Overdue &gt; <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;已过期:   %d\n&quot;</span>, stats.Overdue)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 完成率</span></span><br><span class="line">    rate := <span class="type">float64</span>(stats.Done) / <span class="type">float64</span>(stats.Total) * <span class="number">100</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;完成率:   %.1f%%\n&quot;</span>, rate)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 按分类统计</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n按分类:&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> cat, count := <span class="keyword">range</span> stats.ByCategory &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: %d 个\n&quot;</span>, cat, count)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 按优先级统计</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n按优先级:&quot;</span>)</span><br><span class="line">    <span class="comment">// 按 高→中→低 的顺序输出</span></span><br><span class="line">    <span class="keyword">for</span> _, p := <span class="keyword">range</span> []<span class="type">string</span>&#123;<span class="string">&quot;高&quot;</span>, <span class="string">&quot;中&quot;</span>, <span class="string">&quot;低&quot;</span>&#125; &#123;</span><br><span class="line">        <span class="keyword">if</span> count, ok := stats.ByPriority[p]; ok &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  %s: %d 个\n&quot;</span>, p, count)</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><strong>注意遍历顺序</strong>：直接遍历 <code>map</code> 的顺序是随机的（第 10 课）。所以按优先级输出时，手动指定了遍历顺序 <code>[]string&#123;&quot;高&quot;, &quot;中&quot;, &quot;低&quot;&#125;</code>。</p><hr><h2 id="7-完整代码">7. 完整代码</h2><p>把上面所有代码合并到一个 <code>main.go</code> 文件中：</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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br><span class="line">399</span><br><span class="line">400</span><br><span class="line">401</span><br><span class="line">402</span><br><span class="line">403</span><br><span class="line">404</span><br><span class="line">405</span><br><span class="line">406</span><br><span class="line">407</span><br><span class="line">408</span><br><span class="line">409</span><br><span class="line">410</span><br><span class="line">411</span><br><span class="line">412</span><br><span class="line">413</span><br><span class="line">414</span><br><span class="line">415</span><br><span class="line">416</span><br><span class="line">417</span><br><span class="line">418</span><br><span class="line">419</span><br><span class="line">420</span><br><span class="line">421</span><br><span class="line">422</span><br><span class="line">423</span><br><span class="line">424</span><br><span class="line">425</span><br><span class="line">426</span><br><span class="line">427</span><br><span class="line">428</span><br><span class="line">429</span><br><span class="line">430</span><br><span class="line">431</span><br><span class="line">432</span><br><span class="line">433</span><br><span class="line">434</span><br><span class="line">435</span><br><span class="line">436</span><br><span class="line">437</span><br><span class="line">438</span><br><span class="line">439</span><br><span class="line">440</span><br><span class="line">441</span><br><span class="line">442</span><br><span class="line">443</span><br><span class="line">444</span><br><span class="line">445</span><br><span class="line">446</span><br><span class="line">447</span><br><span class="line">448</span><br><span class="line">449</span><br><span class="line">450</span><br><span class="line">451</span><br><span class="line">452</span><br><span class="line">453</span><br><span class="line">454</span><br><span class="line">455</span><br><span class="line">456</span><br><span class="line">457</span><br><span class="line">458</span><br><span class="line">459</span><br><span class="line">460</span><br><span class="line">461</span><br><span class="line">462</span><br><span class="line">463</span><br><span class="line">464</span><br><span class="line">465</span><br><span class="line">466</span><br><span class="line">467</span><br><span class="line">468</span><br><span class="line">469</span><br><span class="line">470</span><br><span class="line">471</span><br><span class="line">472</span><br><span class="line">473</span><br><span class="line">474</span><br><span class="line">475</span><br><span class="line">476</span><br><span class="line">477</span><br><span class="line">478</span><br><span class="line">479</span><br><span class="line">480</span><br><span class="line">481</span><br><span class="line">482</span><br><span class="line">483</span><br><span class="line">484</span><br><span class="line">485</span><br><span class="line">486</span><br><span class="line">487</span><br><span class="line">488</span><br><span class="line">489</span><br><span class="line">490</span><br><span class="line">491</span><br><span class="line">492</span><br><span class="line">493</span><br><span class="line">494</span><br><span class="line">495</span><br><span class="line">496</span><br><span class="line">497</span><br><span class="line">498</span><br><span class="line">499</span><br><span class="line">500</span><br><span class="line">501</span><br><span class="line">502</span><br><span class="line">503</span><br><span class="line">504</span><br><span class="line">505</span><br><span class="line">506</span><br><span class="line">507</span><br><span class="line">508</span><br><span class="line">509</span><br><span class="line">510</span><br><span class="line">511</span><br><span class="line">512</span><br><span class="line">513</span><br><span class="line">514</span><br><span class="line">515</span><br><span class="line">516</span><br><span class="line">517</span><br><span class="line">518</span><br><span class="line">519</span><br><span class="line">520</span><br><span class="line">521</span><br><span class="line">522</span><br><span class="line">523</span><br><span class="line">524</span><br><span class="line">525</span><br><span class="line">526</span><br><span class="line">527</span><br><span class="line">528</span><br><span class="line">529</span><br><span class="line">530</span><br><span class="line">531</span><br><span class="line">532</span><br><span class="line">533</span><br><span class="line">534</span><br><span class="line">535</span><br><span class="line">536</span><br><span class="line">537</span><br><span class="line">538</span><br><span class="line">539</span><br><span class="line">540</span><br><span class="line">541</span><br><span class="line">542</span><br><span class="line">543</span><br><span class="line">544</span><br><span class="line">545</span><br><span class="line">546</span><br><span class="line">547</span><br><span class="line">548</span><br><span class="line">549</span><br><span class="line">550</span><br><span class="line">551</span><br><span class="line">552</span><br><span class="line">553</span><br><span class="line">554</span><br><span class="line">555</span><br><span class="line">556</span><br><span class="line">557</span><br><span class="line">558</span><br><span class="line">559</span><br><span class="line">560</span><br><span class="line">561</span><br><span class="line">562</span><br><span class="line">563</span><br><span class="line">564</span><br><span class="line">565</span><br><span class="line">566</span><br><span class="line">567</span><br><span class="line">568</span><br><span class="line">569</span><br><span class="line">570</span><br><span class="line">571</span><br><span class="line">572</span><br><span class="line">573</span><br><span class="line">574</span><br><span class="line">575</span><br><span class="line">576</span><br><span class="line">577</span><br><span class="line">578</span><br><span class="line">579</span><br><span class="line">580</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;bufio&quot;</span></span><br><span class="line">    <span class="string">&quot;encoding/json&quot;</span></span><br><span class="line">    <span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;os&quot;</span></span><br><span class="line">    <span class="string">&quot;sort&quot;</span></span><br><span class="line">    <span class="string">&quot;strconv&quot;</span></span><br><span class="line">    <span class="string">&quot;strings&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="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 常量与数据结构</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> dataFile = <span class="string">&quot;todos.json&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    PriorityHigh   = <span class="number">1</span></span><br><span class="line">    PriorityMedium = <span class="number">2</span></span><br><span class="line">    PriorityLow    = <span class="number">3</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">PriorityText</span><span class="params">(p <span class="type">int</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    <span class="keyword">switch</span> p &#123;</span><br><span class="line">    <span class="keyword">case</span> PriorityHigh:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;高&quot;</span></span><br><span class="line">    <span class="keyword">case</span> PriorityMedium:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;中&quot;</span></span><br><span class="line">    <span class="keyword">case</span> PriorityLow:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;低&quot;</span></span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;未知&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID        <span class="type">int</span>       <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Title     <span class="type">string</span>    <span class="string">`json:&quot;title&quot;`</span></span><br><span class="line">    Category  <span class="type">string</span>    <span class="string">`json:&quot;category&quot;`</span></span><br><span class="line">    Priority  <span class="type">int</span>       <span class="string">`json:&quot;priority&quot;`</span></span><br><span class="line">    Done      <span class="type">bool</span>      <span class="string">`json:&quot;done&quot;`</span></span><br><span class="line">    CreatedAt time.Time <span class="string">`json:&quot;created_at&quot;`</span></span><br><span class="line">    DueDate   time.Time <span class="string">`json:&quot;due_date,omitempty&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="params">(t Task)</span></span> String() <span class="type">string</span> &#123;</span><br><span class="line">    status := <span class="string">&quot;[ ]&quot;</span></span><br><span class="line">    <span class="keyword">if</span> t.Done &#123;</span><br><span class="line">        status = <span class="string">&quot;[v]&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    due := <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">if</span> !t.DueDate.IsZero() &#123;</span><br><span class="line">        due = fmt.Sprintf(<span class="string">&quot; | 截止: %s&quot;</span>, t.DueDate.Format(<span class="string">&quot;2006-01-02&quot;</span>))</span><br><span class="line">        <span class="keyword">if</span> !t.Done &amp;&amp; time.Now().After(t.DueDate) &#123;</span><br><span class="line">            due += <span class="string">&quot; (已过期!)&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;%s #%d [%s] %s (%s)%s&quot;</span>,</span><br><span class="line">        status, t.ID, PriorityText(t.Priority), t.Title, t.Category, due)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> TodoList <span class="keyword">struct</span> &#123;</span><br><span class="line">    NextID <span class="type">int</span>    <span class="string">`json:&quot;next_id&quot;`</span></span><br><span class="line">    Tasks  []Task <span class="string">`json:&quot;tasks&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">NewTodoList</span><span class="params">()</span></span> *TodoList &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;TodoList&#123;</span><br><span class="line">        NextID: <span class="number">1</span>,</span><br><span class="line">        Tasks:  []Task&#123;&#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 核心功能</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> AddTask(title, category <span class="type">string</span>, priority <span class="type">int</span>, dueDate time.Time) &#123;</span><br><span class="line">    task := Task&#123;</span><br><span class="line">        ID:        tl.NextID,</span><br><span class="line">        Title:     title,</span><br><span class="line">        Category:  category,</span><br><span class="line">        Priority:  priority,</span><br><span class="line">        Done:      <span class="literal">false</span>,</span><br><span class="line">        CreatedAt: time.Now(),</span><br><span class="line">        DueDate:   dueDate,</span><br><span class="line">    &#125;</span><br><span class="line">    tl.Tasks = <span class="built_in">append</span>(tl.Tasks, task)</span><br><span class="line">    tl.NextID++</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">(tl *TodoList)</span></span> CompleteTask(id <span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].ID == id &#123;</span><br><span class="line">            <span class="keyword">if</span> tl.Tasks[i].Done &#123;</span><br><span class="line">                <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;任务 #%d 已经完成过了&quot;</span>, id)</span><br><span class="line">            &#125;</span><br><span class="line">            tl.Tasks[i].Done = <span class="literal">true</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;找不到任务 #%d&quot;</span>, id)</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">(tl *TodoList)</span></span> DeleteTask(id <span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> i, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.ID == id &#123;</span><br><span class="line">            tl.Tasks = <span class="built_in">append</span>(tl.Tasks[:i], tl.Tasks[i+<span class="number">1</span>:]...)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;找不到任务 #%d&quot;</span>, id)</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">(tl *TodoList)</span></span> SearchTasks(keyword <span class="type">string</span>) []Task &#123;</span><br><span class="line">    keyword = strings.ToLower(keyword)</span><br><span class="line">    result := []Task&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> strings.Contains(strings.ToLower(task.Title), keyword) ||</span><br><span class="line">            strings.Contains(strings.ToLower(task.Category), keyword) &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">(tl *TodoList)</span></span> GetByCategory(category <span class="type">string</span>) []Task &#123;</span><br><span class="line">    result := []Task&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.Category == category &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">(tl *TodoList)</span></span> GetCategories() []<span class="type">string</span> &#123;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    categories := []<span class="type">string</span>&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[task.Category] &#123;</span><br><span class="line">            seen[task.Category] = <span class="literal">true</span></span><br><span class="line">            categories = <span class="built_in">append</span>(categories, task.Category)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> categories</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 排序</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> SortByPriority() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].Done != tl.Tasks[j].Done &#123;</span><br><span class="line">            <span class="keyword">return</span> !tl.Tasks[i].Done</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> tl.Tasks[i].Priority &lt; tl.Tasks[j].Priority</span><br><span class="line">    &#125;)</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">(tl *TodoList)</span></span> SortByDueDate() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> tl.Tasks[i].Done != tl.Tasks[j].Done &#123;</span><br><span class="line">            <span class="keyword">return</span> !tl.Tasks[i].Done</span><br><span class="line">        &#125;</span><br><span class="line">        iHasDue := !tl.Tasks[i].DueDate.IsZero()</span><br><span class="line">        jHasDue := !tl.Tasks[j].DueDate.IsZero()</span><br><span class="line">        <span class="keyword">if</span> iHasDue != jHasDue &#123;</span><br><span class="line">            <span class="keyword">return</span> iHasDue</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> iHasDue &amp;&amp; jHasDue &#123;</span><br><span class="line">            <span class="keyword">return</span> tl.Tasks[i].DueDate.Before(tl.Tasks[j].DueDate)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    &#125;)</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">(tl *TodoList)</span></span> SortByCreatedAt() &#123;</span><br><span class="line">    sort.SliceStable(tl.Tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> tl.Tasks[i].CreatedAt.After(tl.Tasks[j].CreatedAt)</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 统计</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Stats <span class="keyword">struct</span> &#123;</span><br><span class="line">    Total      <span class="type">int</span></span><br><span class="line">    Done       <span class="type">int</span></span><br><span class="line">    Pending    <span class="type">int</span></span><br><span class="line">    Overdue    <span class="type">int</span></span><br><span class="line">    ByCategory <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span></span><br><span class="line">    ByPriority <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</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">(tl *TodoList)</span></span> GetStats() Stats &#123;</span><br><span class="line">    stats := Stats&#123;</span><br><span class="line">        Total:      <span class="built_in">len</span>(tl.Tasks),</span><br><span class="line">        ByCategory: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>),</span><br><span class="line">        ByPriority: <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>),</span><br><span class="line">    &#125;</span><br><span class="line">    now := time.Now()</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> task.Done &#123;</span><br><span class="line">            stats.Done++</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            stats.Pending++</span><br><span class="line">            <span class="keyword">if</span> !task.DueDate.IsZero() &amp;&amp; now.After(task.DueDate) &#123;</span><br><span class="line">                stats.Overdue++</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        stats.ByCategory[task.Category]++</span><br><span class="line">        stats.ByPriority[PriorityText(task.Priority)]++</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> stats</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 文件持久化</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tl *TodoList)</span></span> Save() <span class="type">error</span> &#123;</span><br><span class="line">    data, err := json.MarshalIndent(tl, <span class="string">&quot;&quot;</span>, <span class="string">&quot;  &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;序列化失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">    err = os.WriteFile(dataFile, data, <span class="number">0644</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;写入文件失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</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">Load</span><span class="params">()</span></span> (*TodoList, <span class="type">error</span>) &#123;</span><br><span class="line">    data, err := os.ReadFile(dataFile)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> os.IsNotExist(err) &#123;</span><br><span class="line">            <span class="keyword">return</span> NewTodoList(), <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;读取文件失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> tl TodoList</span><br><span class="line">    err = json.Unmarshal(data, &amp;tl)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;解析 JSON 失败: %w&quot;</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> &amp;tl, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 命令行交互</span></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">printMenu</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println()</span><br><span class="line">    fmt.Println(<span class="string">&quot;========== 待办事项管理器 ==========&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  1. 添加任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  2. 查看所有任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  3. 标记任务完成&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  4. 删除任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  5. 搜索任务&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  6. 按分类查看&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  7. 统计信息&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  0. 保存并退出&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;====================================&quot;</span>)</span><br><span class="line">    fmt.Print(<span class="string">&quot;请选择操作: &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">handleAdd</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 添加任务 ---&quot;</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;任务标题: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    title := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> title == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;标题不能为空&quot;</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">    fmt.Print(<span class="string">&quot;分类 (如: 工作/学习/生活，默认: 其他): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    category := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> category == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        category = <span class="string">&quot;其他&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;优先级 (1=高 2=中 3=低，默认: 2): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    priorityStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    priority := PriorityMedium</span><br><span class="line">    <span class="keyword">if</span> priorityStr != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        p, err := strconv.Atoi(priorityStr)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> || p &lt; <span class="number">1</span> || p &gt; <span class="number">3</span> &#123;</span><br><span class="line">            fmt.Println(<span class="string">&quot;优先级无效，使用默认值(中)&quot;</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            priority = p</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;截止日期 (格式: 2006-01-02，留空跳过): &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    dueDateStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">var</span> dueDate time.Time</span><br><span class="line">    <span class="keyword">if</span> dueDateStr != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        parsed, err := time.Parse(<span class="string">&quot;2006-01-02&quot;</span>, dueDateStr)</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;日期格式不对，跳过截止日期&quot;</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            dueDate = parsed.Add(<span class="number">23</span>*time.Hour + <span class="number">59</span>*time.Minute + <span class="number">59</span>*time.Second)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    tl.AddTask(title, category, priority, dueDate)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;任务已添加: #%d %s\n&quot;</span>, tl.NextID<span class="number">-1</span>, title)</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">handleList</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(tl.Tasks) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n排序方式:&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  1. 按优先级&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  2. 按截止日期&quot;</span>)</span><br><span class="line">    fmt.Println(<span class="string">&quot;  3. 按创建时间&quot;</span>)</span><br><span class="line">    fmt.Print(<span class="string">&quot;选择 (默认: 1): &quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    sortChoice := strings.TrimSpace(scanner.Text())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">switch</span> sortChoice &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;2&quot;</span>:</span><br><span class="line">        tl.SortByDueDate()</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;3&quot;</span>:</span><br><span class="line">        tl.SortByCreatedAt()</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        tl.SortByPriority()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 任务列表 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n共 %d 个任务\n&quot;</span>, <span class="built_in">len</span>(tl.Tasks))</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">handleComplete</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    pending := []Task&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        <span class="keyword">if</span> !task.Done &#123;</span><br><span class="line">            pending = <span class="built_in">append</span>(pending, task)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(pending) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有未完成的任务&quot;</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">    fmt.Println(<span class="string">&quot;\n--- 未完成的任务 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> pending &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入要完成的任务编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    id, err := strconv.Atoi(idStr)</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;请输入有效的数字&quot;</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">    err = tl.CompleteTask(id)</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;操作失败: %v\n&quot;</span>, err)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;任务 #%d 已标记为完成!\n&quot;</span>, id)</span><br><span class="line">    &#125;</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">handleDelete</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(tl.Tasks) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n--- 所有任务 ---&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入要删除的任务编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    id, err := strconv.Atoi(idStr)</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;请输入有效的数字&quot;</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">    fmt.Printf(<span class="string">&quot;确认删除任务 #%d? (y/n): &quot;</span>, id)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    confirm := strings.TrimSpace(strings.ToLower(scanner.Text()))</span><br><span class="line">    <span class="keyword">if</span> confirm != <span class="string">&quot;y&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;已取消删除&quot;</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">    err = tl.DeleteTask(id)</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;删除失败: %v\n&quot;</span>, err)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;任务 #%d 已删除\n&quot;</span>, id)</span><br><span class="line">    &#125;</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">handleSearch</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    fmt.Print(<span class="string">&quot;\n输入搜索关键字: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    keyword := strings.TrimSpace(scanner.Text())</span><br><span class="line">    <span class="keyword">if</span> keyword == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;关键字不能为空&quot;</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">    results := tl.SearchTasks(keyword)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(results) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;没有找到包含 \&quot;%s\&quot; 的任务\n&quot;</span>, keyword)</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">    fmt.Printf(<span class="string">&quot;\n--- 搜索结果 (%d 条) ---\n&quot;</span>, <span class="built_in">len</span>(results))</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> results &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</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">handleCategoryView</span><span class="params">(tl *TodoList, scanner *bufio.Scanner)</span></span> &#123;</span><br><span class="line">    categories := tl.GetCategories()</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(categories) == <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;\n没有任何任务&quot;</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">    fmt.Println(<span class="string">&quot;\n可用分类:&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> i, cat := <span class="keyword">range</span> categories &#123;</span><br><span class="line">        count := <span class="built_in">len</span>(tl.GetByCategory(cat))</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %d. %s (%d 个任务)\n&quot;</span>, i+<span class="number">1</span>, cat, count)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Print(<span class="string">&quot;\n选择分类编号: &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    idxStr := strings.TrimSpace(scanner.Text())</span><br><span class="line">    idx, err := strconv.Atoi(idxStr)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> || idx &lt; <span class="number">1</span> || idx &gt; <span class="built_in">len</span>(categories) &#123;</span><br><span class="line">        fmt.Println(<span class="string">&quot;无效的选择&quot;</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">    category := categories[idx<span class="number">-1</span>]</span><br><span class="line">    tasks := tl.GetByCategory(category)</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;\n--- 分类: %s ---\n&quot;</span>, category)</span><br><span class="line">    <span class="keyword">for</span> _, task := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">        fmt.Println(task)</span><br><span class="line">    &#125;</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">handleStats</span><span class="params">(tl *TodoList)</span></span> &#123;</span><br><span class="line">    stats := tl.GetStats()</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n--- 统计信息 ---&quot;</span>)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;总任务数: %d\n&quot;</span>, stats.Total)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> stats.Total == <span class="number">0</span> &#123;</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">    fmt.Printf(<span class="string">&quot;已完成:   %d\n&quot;</span>, stats.Done)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;未完成:   %d\n&quot;</span>, stats.Pending)</span><br><span class="line">    <span class="keyword">if</span> stats.Overdue &gt; <span class="number">0</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;已过期:   %d\n&quot;</span>, stats.Overdue)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    rate := <span class="type">float64</span>(stats.Done) / <span class="type">float64</span>(stats.Total) * <span class="number">100</span></span><br><span class="line">    fmt.Printf(<span class="string">&quot;完成率:   %.1f%%\n&quot;</span>, rate)</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n按分类:&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> cat, count := <span class="keyword">range</span> stats.ByCategory &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: %d 个\n&quot;</span>, cat, count)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n按优先级:&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, p := <span class="keyword">range</span> []<span class="type">string</span>&#123;<span class="string">&quot;高&quot;</span>, <span class="string">&quot;中&quot;</span>, <span class="string">&quot;低&quot;</span>&#125; &#123;</span><br><span class="line">        <span class="keyword">if</span> count, ok := stats.ByPriority[p]; ok &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  %s: %d 个\n&quot;</span>, p, count)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ============================================================</span></span><br><span class="line"><span class="comment">// 主函数</span></span><br><span class="line"><span class="comment">// ============================================================</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">    todoList, err := Load()</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;加载数据失败: %v\n&quot;</span>, err)</span><br><span class="line">        fmt.Println(<span class="string">&quot;将使用空列表启动&quot;</span>)</span><br><span class="line">        todoList = NewTodoList()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    scanner := bufio.NewScanner(os.Stdin)</span><br><span class="line">    fmt.Println(<span class="string">&quot;欢迎使用待办事项管理器！&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        printMenu()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> !scanner.Scan() &#123;</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">        choice := strings.TrimSpace(scanner.Text())</span><br><span class="line"></span><br><span class="line">        <span class="keyword">switch</span> choice &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;1&quot;</span>:</span><br><span class="line">            handleAdd(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;2&quot;</span>:</span><br><span class="line">            handleList(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;3&quot;</span>:</span><br><span class="line">            handleComplete(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;4&quot;</span>:</span><br><span class="line">            handleDelete(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;5&quot;</span>:</span><br><span class="line">            handleSearch(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;6&quot;</span>:</span><br><span class="line">            handleCategoryView(todoList, scanner)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;7&quot;</span>:</span><br><span class="line">            handleStats(todoList)</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&quot;0&quot;</span>:</span><br><span class="line">            err := todoList.Save()</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;保存失败: %v\n&quot;</span>, err)</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                fmt.Println(<span class="string">&quot;数据已保存，再见！&quot;</span>)</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Println(<span class="string">&quot;无效的选项，请重新选择&quot;</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><hr><h2 id="8-运行效果">8. 运行效果</h2><p>把代码保存为 <code>main.go</code>，运行 <code>go run main.go</code>，实际操作一把：</p><figure class="highlight nestedtext"><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></pre></td><td class="code"><pre><span class="line"><span class="attribute">欢迎使用待办事项管理器！</span></span><br><span class="line"><span class="attribute"></span></span><br><span class="line"><span class="attribute">========== 待办事项管理器 ==========</span></span><br><span class="line"><span class="attribute">  1. 添加任务</span></span><br><span class="line"><span class="attribute">  2. 查看所有任务</span></span><br><span class="line"><span class="attribute">  3. 标记任务完成</span></span><br><span class="line"><span class="attribute">  4. 删除任务</span></span><br><span class="line"><span class="attribute">  5. 搜索任务</span></span><br><span class="line"><span class="attribute">  6. 按分类查看</span></span><br><span class="line"><span class="attribute">  7. 统计信息</span></span><br><span class="line"><span class="attribute">  0. 保存并退出</span></span><br><span class="line"><span class="attribute">====================================</span></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">1</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">--- 添加任务 ---</span></span><br><span class="line"><span class="attribute">任务标题</span><span class="punctuation">:</span> <span class="string">学完 Go 第 24 课</span></span><br><span class="line"><span class="attribute">分类 (如</span><span class="punctuation">:</span> <span class="string">工作/学习/生活，默认: 其他): 学习</span></span><br><span class="line"><span class="attribute">优先级 (1=高 2=中 3=低，默认</span><span class="punctuation">:</span> <span class="string">2): 1</span></span><br><span class="line"><span class="attribute">截止日期 (格式</span><span class="punctuation">:</span> <span class="string">2006-01-02，留空跳过): 2025-01-20</span></span><br><span class="line"><span class="attribute">任务已添加</span><span class="punctuation">:</span> <span class="string">#1 学完 Go 第 24 课</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">========== 待办事项管理器 ==========</span></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">1</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">--- 添加任务 ---</span></span><br><span class="line"><span class="attribute">任务标题</span><span class="punctuation">:</span> <span class="string">买菜做饭</span></span><br><span class="line"><span class="attribute">分类 (如</span><span class="punctuation">:</span> <span class="string">工作/学习/生活，默认: 其他): 生活</span></span><br><span class="line"><span class="attribute">优先级 (1=高 2=中 3=低，默认</span><span class="punctuation">:</span> <span class="string">2):</span></span><br><span class="line"><span class="attribute">截止日期 (格式</span><span class="punctuation">:</span> <span class="string">2006-01-02，留空跳过):</span></span><br><span class="line"><span class="attribute">任务已添加</span><span class="punctuation">:</span> <span class="string">#2 买菜做饭</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">2</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">排序方式</span><span class="punctuation">:</span></span><br><span class="line">  <span class="attribute">1. 按优先级</span></span><br><span class="line"><span class="attribute">  2. 按截止日期</span></span><br><span class="line"><span class="attribute">  3. 按创建时间</span></span><br><span class="line"><span class="attribute">选择 (默认</span><span class="punctuation">:</span> <span class="string">1): 1</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">--- 任务列表 ---</span></span><br><span class="line"><span class="attribute">[ ] #1 [高] 学完 Go 第 24 课 (学习) | 截止</span><span class="punctuation">:</span> <span class="string">2025-01-20</span></span><br><span class="line"><span class="attribute">[ ] #2 [中] 买菜做饭 (生活)</span></span><br><span class="line"><span class="attribute"></span></span><br><span class="line"><span class="attribute">共 2 个任务</span></span><br><span class="line"><span class="attribute"></span></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">3</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">--- 未完成的任务 ---</span></span><br><span class="line"><span class="attribute">[ ] #1 [高] 学完 Go 第 24 课 (学习) | 截止</span><span class="punctuation">:</span> <span class="string">2025-01-20</span></span><br><span class="line"><span class="attribute">[ ] #2 [中] 买菜做饭 (生活)</span></span><br><span class="line"><span class="attribute"></span></span><br><span class="line"><span class="attribute">输入要完成的任务编号</span><span class="punctuation">:</span> <span class="string">2</span></span><br><span class="line"><span class="attribute">任务 #2 已标记为完成!</span></span><br><span class="line"><span class="attribute"></span></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">7</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">--- 统计信息 ---</span></span><br><span class="line"><span class="attribute">总任务数</span><span class="punctuation">:</span> <span class="string">2</span></span><br><span class="line"><span class="attribute">已完成</span><span class="punctuation">:</span> <span class="string">  1</span></span><br><span class="line"><span class="attribute">未完成</span><span class="punctuation">:</span> <span class="string">  1</span></span><br><span class="line"><span class="attribute">完成率</span><span class="punctuation">:</span> <span class="string">  50.0%</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">按分类</span><span class="punctuation">:</span></span><br><span class="line">  <span class="attribute">学习</span><span class="punctuation">:</span> <span class="string">1 个</span></span><br><span class="line">  <span class="attribute">生活</span><span class="punctuation">:</span> <span class="string">1 个</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">按优先级</span><span class="punctuation">:</span></span><br><span class="line">  <span class="attribute">高</span><span class="punctuation">:</span> <span class="string">1 个</span></span><br><span class="line">  <span class="attribute">中</span><span class="punctuation">:</span> <span class="string">1 个</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">请选择操作</span><span class="punctuation">:</span> <span class="string">0</span></span><br><span class="line">数据已保存，再见！</span><br></pre></td></tr></table></figure><p>退出后再启动程序，数据还在——因为保存到了 <code>todos.json</code> 文件里。</p><hr><h2 id="9-项目回顾——学到了什么">9. 项目回顾——学到了什么</h2><p>这个项目虽然不大，但已经包含了一个真实程序的完整要素。回顾一下每个关键设计决策：</p><h3 id="9-1-为什么用-NextID-而不是数组索引做-ID">9.1 为什么用 <code>NextID</code> 而不是数组索引做 ID</h3><p>如果用数组索引做 ID，删除一个任务后，后面所有任务的 ID 都会变。用户说&quot;完成第 3 号任务&quot;，删了中间一个后第 3 号指向的任务就变了。<code>NextID</code> 只增不减，保证每个任务有唯一稳定的编号。</p><h3 id="9-2-为什么-CompleteTask-用索引遍历而不用-range-值">9.2 为什么 <code>CompleteTask</code> 用索引遍历而不用 range 值</h3><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="comment">// 错误：修改的是副本</span></span><br><span class="line"><span class="keyword">for</span> _, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">    <span class="keyword">if</span> task.ID == id &#123;</span><br><span class="line">        task.Done = <span class="literal">true</span>  <span class="comment">// 改了副本，原数据不变！</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：通过索引修改原数据</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">    <span class="keyword">if</span> tl.Tasks[i].ID == id &#123;</span><br><span class="line">        tl.Tasks[i].Done = <span class="literal">true</span>  <span class="comment">// 直接修改切片中的元素</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是第 9 课和第 12 课反复强调的：<code>range</code> 的第二个值是副本。</p><h3 id="9-3-为什么用-bufio-Scanner-读输入">9.3 为什么用 <code>bufio.Scanner</code> 读输入</h3><p><code>fmt.Scan</code> 以空白为分隔符，如果用户输入&quot;写 Go 作业&quot;，<code>fmt.Scan</code> 只能读到&quot;写&quot;。<code>bufio.Scanner</code> 按行读取，能处理完整的一行输入。</p><h3 id="9-4-为什么每个-handle-函数都传-scanner">9.4 为什么每个 handle 函数都传 scanner</h3><p>因为 <code>bufio.Scanner</code> 包装了 <code>os.Stdin</code>。如果在多个地方各自创建 Scanner，它们的内部缓冲区会互相抢数据，导致读取混乱。<strong>一个 stdin 只用一个 Scanner</strong>。</p><h3 id="9-5-为什么排序用-SliceStable-而不是-Slice">9.5 为什么排序用 SliceStable 而不是 Slice</h3><p>用户操作任务时有个朴素的期望：如果两个任务优先级一样，它们应该保持我添加时的顺序。<code>SliceStable</code> 满足这个期望，<code>Slice</code> 不保证。</p><h3 id="9-6-保存时机的选择">9.6 保存时机的选择</h3><p>当前实现是退出时保存。这意味着如果程序崩溃或者被强制关闭，数据会丢失。更稳妥的做法是每次修改后都自动保存，但那样代码更复杂。对于学习项目，退出时保存够用了。</p><hr><h2 id="10-常见坑总结">10. 常见坑总结</h2><h3 id="10-1-Scanner-的缓冲区大小">10.1 Scanner 的缓冲区大小</h3><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">scanner := bufio.NewScanner(os.Stdin)</span><br><span class="line"><span class="comment">// 默认缓冲区大小是 64KB</span></span><br><span class="line"><span class="comment">// 如果一行输入超过 64KB，scanner.Scan() 会返回 false</span></span><br><span class="line"><span class="comment">// 对于命令行交互来说绰绰有余</span></span><br></pre></td></tr></table></figure><h3 id="10-2-time-Parse-的参考时间">10.2 time.Parse 的参考时间</h3><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"><span class="comment">// Go 的时间解析用的参考时间是固定的：2006-01-02 15:04:05</span></span><br><span class="line"><span class="comment">// 不是随便写的，而是 Go 的&quot;诞生时间&quot;</span></span><br><span class="line"><span class="comment">// 如果格式写错了，解析结果就是错的</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确</span></span><br><span class="line">time.Parse(<span class="string">&quot;2006-01-02&quot;</span>, <span class="string">&quot;2025-01-20&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 错误——格式字符串用了错误的数字</span></span><br><span class="line">time.Parse(<span class="string">&quot;2023-01-02&quot;</span>, <span class="string">&quot;2025-01-20&quot;</span>)  <span class="comment">// 解析结果不对</span></span><br></pre></td></tr></table></figure><h3 id="10-3-JSON-的零值问题">10.3 JSON 的零值问题</h3><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">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    DueDate time.Time <span class="string">`json:&quot;due_date,omitempty&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// omitempty 对 time.Time 的判断是 IsZero()</span></span><br><span class="line"><span class="comment">// 即 0001-01-01T00:00:00Z</span></span><br><span class="line"><span class="comment">// 没设截止日期的任务不会输出 due_date 字段</span></span><br></pre></td></tr></table></figure><h3 id="10-4-切片删除后的索引">10.4 切片删除后的索引</h3><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"><span class="comment">// 删除后不要继续用原来的索引遍历</span></span><br><span class="line"><span class="keyword">for</span> i, task := <span class="keyword">range</span> tl.Tasks &#123;</span><br><span class="line">    <span class="keyword">if</span> shouldDelete(task) &#123;</span><br><span class="line">        tl.Tasks = <span class="built_in">append</span>(tl.Tasks[:i], tl.Tasks[i+<span class="number">1</span>:]...)</span><br><span class="line">        <span class="comment">// 这里应该 return 或 break</span></span><br><span class="line">        <span class="comment">// 如果继续循环，索引 i+1 对应的元素已经变了</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个项目里每次只删一个，删完就 <code>return</code>，所以没有问题。但如果你要批量删除，需要从后往前遍历，或者用新切片收集要保留的元素。</p><h3 id="10-5-map-遍历顺序不确定">10.5 map 遍历顺序不确定</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误：直接遍历 map，每次顺序可能不同</span></span><br><span class="line"><span class="keyword">for</span> cat, count := <span class="keyword">range</span> stats.ByCategory &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s: %d\n&quot;</span>, cat, count)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果需要固定顺序，先收集 key，排序后遍历</span></span><br><span class="line">keys := <span class="built_in">make</span>([]<span class="type">string</span>, <span class="number">0</span>, <span class="built_in">len</span>(stats.ByCategory))</span><br><span class="line"><span class="keyword">for</span> k := <span class="keyword">range</span> stats.ByCategory &#123;</span><br><span class="line">    keys = <span class="built_in">append</span>(keys, k)</span><br><span class="line">&#125;</span><br><span class="line">sort.Strings(keys)</span><br><span class="line"><span class="keyword">for</span> _, k := <span class="keyword">range</span> keys &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s: %d\n&quot;</span>, k, stats.ByCategory[k])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="10-6-输入校验不能省">10.6 输入校验不能省</h3><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="comment">// 如果用户输入的不是数字，strconv.Atoi 会返回错误</span></span><br><span class="line">id, err := strconv.Atoi(idStr)</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;请输入有效的数字&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span>  <span class="comment">// 别忘了 return，否则会用零值继续执行</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>每个用户输入都要假设它可能是错的。不做校验的程序迟早会崩。</p><hr><h2 id="11-本课练习">11. 本课练习</h2><h3 id="练习-1：添加-修改任务-功能">练习 1：添加&quot;修改任务&quot;功能</h3><p>要求：</p><ul><li>用户可以修改已有任务的标题、分类、优先级、截止日期</li><li>先显示当前值，用户输入新值（留空表示不修改）</li></ul><p>提示：找到任务后逐个字段提示用户输入，空输入跳过。</p><hr><h3 id="练习-2：添加-批量完成-功能">练习 2：添加&quot;批量完成&quot;功能</h3><p>要求：</p><ul><li>用户可以一次性输入多个任务编号，用逗号分隔（如 <code>1,3,5</code>）</li><li>逐个处理，输出每个任务的处理结果</li></ul><p>提示：用 <code>strings.Split</code> 按逗号分割，循环处理每个编号。</p><hr><h3 id="练习-3：数据备份功能">练习 3：数据备份功能</h3><p>要求：</p><ul><li>添加一个菜单选项&quot;备份数据&quot;</li><li>把当前数据保存到带日期的文件名，如 <code>todos_20250120.json</code></li><li>用 <code>time.Now().Format(&quot;20060102&quot;)</code> 生成日期字符串</li></ul><hr><h3 id="练习-4：改进排序——支持同时按多个条件排序">练习 4：改进排序——支持同时按多个条件排序</h3><p>要求：</p><ul><li>让用户可以选择&quot;先按分类排，再按优先级排&quot;</li><li>或者&quot;先按完成状态排，再按截止日期排&quot;</li></ul><p>提示：在 <code>less</code> 函数中嵌套条件判断，跟第 23 课的多字段排序一样。</p><hr><h3 id="练习-5：导出为文本报告">练习 5：导出为文本报告</h3><p>要求：</p><ul><li>添加一个菜单选项&quot;导出报告&quot;</li><li>生成一个纯文本的任务报告文件 <code>report.txt</code></li><li>内容包括统计信息和所有任务列表</li></ul><p>提示：用 <code>os.WriteFile</code> 或 <code>bufio.Writer</code> 写文件。</p><hr><h2 id="12-自测题">12. 自测题</h2><h3 id="12-1-概念题">12.1 概念题</h3><ol><li>为什么 <code>AddTask</code> 方法要用指针接收者 <code>*TodoList</code> 而不是值接收者 <code>TodoList</code>？</li><li><code>for _, task := range tl.Tasks</code> 中修改 <code>task.Done</code> 为什么不生效？怎么改？</li><li>为什么用 <code>bufio.Scanner</code> 读输入而不是 <code>fmt.Scan</code>？</li><li><code>os.IsNotExist(err)</code> 判断的是什么？为什么要单独处理这个错误？</li><li><code>json.MarshalIndent(tl, &quot;&quot;, &quot;  &quot;)</code> 的第二个和第三个参数分别是什么意思？</li><li>为什么 <code>NextID</code> 只增不减，即使删了任务也不回收 ID？</li><li>为什么一个 <code>os.Stdin</code> 只应该创建一个 <code>bufio.Scanner</code>？</li><li><code>sort.SliceStable</code> 比 <code>sort.Slice</code> 多保证了什么？在这个项目中为什么选择用它？</li></ol><h3 id="12-2-代码阅读题">12.2 代码阅读题</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Item <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Done <span class="type">bool</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">items := []Item&#123;</span><br><span class="line">    &#123;<span class="string">&quot;A&quot;</span>, <span class="literal">false</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;B&quot;</span>, <span class="literal">false</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;C&quot;</span>, <span class="literal">false</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">    item.Done = <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s: %v\n&quot;</span>, item.Name, item.Done)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><figure class="highlight nix"><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="params">A:</span> <span class="literal">false</span></span><br><span class="line"><span class="params">B:</span> <span class="literal">false</span></span><br><span class="line"><span class="params">C:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>解释：</p><ol><li>第一个 <code>for range</code> 中的 <code>item</code> 是副本，修改 <code>item.Done</code> 不影响切片中的原始数据</li><li>要修改原始数据，必须用索引：<code>items[i].Done = true</code></li><li>这正是本课 <code>CompleteTask</code> 方法中为什么用 <code>for i := range</code> 而不是 <code>for _, task := range</code> 的原因</li></ol></details><hr><h2 id="13-本课总结">13. 本课总结</h2><p>这是阶段四的最后一课，你通过一个完整的项目把前面学的知识串了起来。</p><table><thead><tr><th>你做了什么</th><th>用到了哪些知识</th></tr></thead><tbody><tr><td>定义 Task 结构体</td><td>结构体、JSON tag</td></tr><tr><td>实现 String() 方法</td><td>方法、Stringer 接口、时间格式化</td></tr><tr><td>添加/完成/删除任务</td><td>指针接收者、切片操作、错误处理</td></tr><tr><td>搜索和筛选</td><td>字符串处理、切片过滤</td></tr><tr><td>多种排序方式</td><td>sort.SliceStable、多字段排序</td></tr><tr><td>统计信息</td><td>map 计数、格式化输出</td></tr><tr><td>数据持久化</td><td>JSON 编解码、文件读写</td></tr><tr><td>命令行交互</td><td>bufio.Scanner、strconv、输入校验</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>写项目先想清楚数据结构——结构体和切片是 Go 程序的骨架</strong></li><li><strong>用户输入永远不可信——每个输入都要校验，每个错误都要处理</strong></li><li><strong>知识串起来才有用——单独会排序、会文件操作没用，能组合在一起解决实际问题才算掌握</strong></li></ol><p>恭喜你完成了阶段四！到这里，你已经具备了用 Go 编写真实小项目的完整能力。</p><hr><h2 id="14-下一课预告">14. 下一课预告</h2><p>下一课进入<strong>阶段五：并发与工程阶段</strong>，这是 Go 最有特色的部分。</p><p>第 25 课：<strong>goroutine 入门</strong></p><p>会重点讲：</p><ul><li>Go 并发的基本概念——什么是 goroutine</li><li>怎么创建和运行 goroutine</li><li>主协程退出问题——为什么你的 goroutine “没有执行”</li><li>goroutine 和线程的区别</li></ul><p>学完下一课，你就踏入了 Go 最强大也最有意思的领域。</p>]]></content>
    
    
    <summary type="html">Go语言系列24</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 从 0 到精通 · 第 23 课：排序、集合与常见算法工具</title>
    <link href="https://yjyrichard.github.io/posts/b1a08992.html"/>
    <id>https://yjyrichard.github.io/posts/b1a08992.html</id>
    <published>2026-03-22T15:28:11.709Z</published>
    <updated>2026-03-22T15:40:40.007Z</updated>
    
    <content type="html"><![CDATA[<h1>Go 从 0 到精通 · 第 23 课：排序、集合与常见算法工具</h1><blockquote><p>学习定位：这是整套 Go 教程的第 23 课。<br>前置要求：已经完成第 22 课，掌握了 JSON 编解码。需要熟悉结构体、切片、接口和方法。<br>本课目标：掌握 Go 中 <code>sort</code> 包的使用，能对各种类型的数据进行排序，理解自定义排序的思路，能用切片和 map 实现常见的集合操作（去重、交集、并集等），能对结构化数据进行灵活的排序处理。</p></blockquote><hr><h2 id="1-本课你要解决的核心问题">1. 本课你要解决的核心问题</h2><p>排序和集合操作是编程中最常见的任务。用户列表要按注册时间排序，商品列表要按价格排序，搜索结果要按相关度排序。同时，你经常需要做去重、求交集、过滤等操作。</p><p>你需要搞明白以下问题：</p><ul><li>Go 的排序和其他语言有什么不同</li><li>怎么对整数、字符串切片排序</li><li>怎么按结构体的某个字段排序</li><li>怎么实现多字段排序（先按 A 排，再按 B 排）</li><li>怎么做降序排列</li><li>怎么判断一个切片是否已排序</li><li>怎么在有序切片中查找元素</li><li>怎么用切片和 map 实现集合操作</li></ul><p>学完这一课，你就能自如地处理数据排序和集合操作了。</p><hr><h2 id="2-sort-包总览">2. <code>sort</code> 包总览</h2><p>Go 的排序功能都在 <code>sort</code> 包里。先看核心概念：</p><p><strong>Go 的排序和其他语言最大的不同</strong>：Go 没有对泛型排序的内置支持（在 1.21 之前），而是通过接口来实现排序。你需要理解 <code>sort.Interface</code>。</p><h3 id="2-1-核心类型和函数">2.1 核心类型和函数</h3><table><thead><tr><th>函数/类型</th><th>用途</th></tr></thead><tbody><tr><td><code>sort.Ints([]int)</code></td><td>对 int 切片升序排序</td></tr><tr><td><code>sort.Float64s([]float64)</code></td><td>对 float64 切片升序排序</td></tr><tr><td><code>sort.Strings([]string)</code></td><td>对 string 切片升序排序</td></tr><tr><td><code>sort.Sort(data)</code></td><td>按自定义规则排序（需要实现 <code>sort.Interface</code>）</td></tr><tr><td><code>sort.Slice(s, less)</code></td><td>按自定义函数排序（Go 1.8+，最常用）</td></tr><tr><td><code>sort.SliceStable(s, less)</code></td><td>同上，但保持相等元素的原始顺序</td></tr><tr><td><code>sort.Reverse(data)</code></td><td>反转排序顺序</td></tr><tr><td><code>sort.IsSorted(data)</code></td><td>判断是否已排序</td></tr><tr><td><code>sort.Search(n, f)</code></td><td>二分查找</td></tr></tbody></table><p>下面逐步展开。</p><hr><h2 id="3-基本类型排序">3. 基本类型排序</h2><h3 id="3-1-排序整数切片">3.1 排序整数切片</h3><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="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;sort&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">    nums := []<span class="type">int</span>&#123;<span class="number">5</span>, <span class="number">2</span>, <span class="number">8</span>, <span class="number">1</span>, <span class="number">9</span>, <span class="number">3</span>, <span class="number">7</span>&#125;</span><br><span class="line"></span><br><span class="line">    sort.Ints(nums)</span><br><span class="line">    fmt.Println(nums)  <span class="comment">// [1 2 3 5 7 8 9]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就这么简单。<code>sort.Ints</code> 直接修改原切片，升序排列。</p><h3 id="3-2-排序浮点数切片">3.2 排序浮点数切片</h3><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">prices := []<span class="type">float64</span>&#123;<span class="number">29.9</span>, <span class="number">9.9</span>, <span class="number">59.9</span>, <span class="number">19.9</span>, <span class="number">39.9</span>&#125;</span><br><span class="line"></span><br><span class="line">sort.Float64s(prices)</span><br><span class="line">fmt.Println(prices)  <span class="comment">// [9.9 19.9 29.9 39.9 59.9]</span></span><br></pre></td></tr></table></figure><h3 id="3-3-排序字符串切片">3.3 排序字符串切片</h3><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">names := []<span class="type">string</span>&#123;<span class="string">&quot;Charlie&quot;</span>, <span class="string">&quot;Alice&quot;</span>, <span class="string">&quot;Bob&quot;</span>, <span class="string">&quot;David&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">sort.Strings(names)</span><br><span class="line">fmt.Println(names)  <span class="comment">// [Alice Bob Charlie David]</span></span><br></pre></td></tr></table></figure><p>字符串按字典序（Unicode 编码值）排序。</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="comment">// 中文排序</span></span><br><span class="line">cities := []<span class="type">string</span>&#123;<span class="string">&quot;北京&quot;</span>, <span class="string">&quot;上海&quot;</span>, <span class="string">&quot;广州&quot;</span>, <span class="string">&quot;深圳&quot;</span>&#125;</span><br><span class="line">sort.Strings(cities)</span><br><span class="line">fmt.Println(cities)  <span class="comment">// [北京 广州 上海 深圳]（按拼音首字母排序）</span></span><br></pre></td></tr></table></figure><p>注意：<code>sort.Strings</code> 对中文按 Unicode 编码值排序，结果和拼音排序可能不完全一致。如果需要精确的拼音排序，需要第三方库。</p><h3 id="3-4-排序是原地的">3.4 排序是原地的</h3><p><strong>重要</strong>：所有 <code>sort</code> 包的函数都是<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></pre></td><td class="code"><pre><span class="line">original := []<span class="type">int</span>&#123;<span class="number">3</span>, <span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line">sorted := original</span><br><span class="line"></span><br><span class="line">sort.Ints(sorted)</span><br><span class="line">fmt.Println(original)  <span class="comment">// [1 2 3]（也被修改了！）</span></span><br><span class="line">fmt.Println(sorted)    <span class="comment">// [1 2 3]</span></span><br><span class="line"><span class="comment">// original 和 sorted 指向同一个底层数组</span></span><br></pre></td></tr></table></figure><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">original := []<span class="type">int</span>&#123;<span class="number">3</span>, <span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line">sorted := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="built_in">len</span>(original))</span><br><span class="line"><span class="built_in">copy</span>(sorted, original)</span><br><span class="line"></span><br><span class="line">sort.Ints(sorted)</span><br><span class="line">fmt.Println(original)  <span class="comment">// [3 1 2]（不变）</span></span><br><span class="line">fmt.Println(sorted)    <span class="comment">// [1 2 3]</span></span><br></pre></td></tr></table></figure><hr><h2 id="4-sort-Slice——最灵活的排序方式">4. <code>sort.Slice</code>——最灵活的排序方式</h2><h3 id="4-1-基本用法">4.1 基本用法</h3><p><code>sort.Slice</code> 是 Go 1.8 引入的，它接收一个切片和一个比较函数，是最常用的方式：</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">nums := []<span class="type">int</span>&#123;<span class="number">5</span>, <span class="number">2</span>, <span class="number">8</span>, <span class="number">1</span>, <span class="number">9</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 升序：i 对应的元素 &lt; j 对应的元素</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &lt; nums[j]</span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(nums)  <span class="comment">// [1 2 5 8 9]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 降序</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &gt; nums[j]</span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(nums)  <span class="comment">// [9 8 5 2 1]</span></span><br></pre></td></tr></table></figure><p><code>less</code> 函数接收两个<strong>索引</strong> <code>i</code> 和 <code>j</code>，返回 <code>true</code> 表示索引 <code>i</code> 的元素应该排在索引 <code>j</code> 的元素前面。</p><h3 id="4-2-按结构体字段排序">4.2 按结构体字段排序</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><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">type</span> Student <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name  <span class="type">string</span></span><br><span class="line">    Score <span class="type">int</span></span><br><span class="line">    Age   <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">students := []Student&#123;</span><br><span class="line">    &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">95</span>, <span class="number">20</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Bob&quot;</span>, <span class="number">88</span>, <span class="number">22</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Charlie&quot;</span>, <span class="number">92</span>, <span class="number">19</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;David&quot;</span>, <span class="number">88</span>, <span class="number">21</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 按分数排序（降序）</span></span><br><span class="line">sort.Slice(students, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> students[i].Score &gt; students[j].Score</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, s := <span class="keyword">range</span> students &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s: %d分\n&quot;</span>, s.Name, s.Score)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Alice: 95分</span></span><br><span class="line"><span class="comment">// Charlie: 92分</span></span><br><span class="line"><span class="comment">// Bob: 88分</span></span><br><span class="line"><span class="comment">// David: 88分</span></span><br></pre></td></tr></table></figure><h3 id="4-3-多字段排序">4.3 多字段排序</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><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">sort.Slice(students, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> students[i].Score != students[j].Score &#123;</span><br><span class="line">        <span class="keyword">return</span> students[i].Score &gt; students[j].Score  <span class="comment">// 分数降序</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> students[i].Age &lt; students[j].Age  <span class="comment">// 年龄升序</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, s := <span class="keyword">range</span> students &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;%s: %d分, %d岁\n&quot;</span>, s.Name, s.Score, s.Age)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Alice: 95分, 20岁</span></span><br><span class="line"><span class="comment">// Charlie: 92分, 19岁</span></span><br><span class="line"><span class="comment">// David: 88分, 21岁</span></span><br><span class="line"><span class="comment">// Bob: 88分, 22岁</span></span><br></pre></td></tr></table></figure><p>注意 Bob 和 David：分数都是 88，但 David 年龄小（21 &lt; 22），所以排在前面。</p><h3 id="4-4-sort-Slice-vs-sort-SliceStable">4.4 <code>sort.Slice</code> vs <code>sort.SliceStable</code></h3><p><code>sort.Slice</code> 不保证相等元素的相对顺序。<code>sort.SliceStable</code> 保持相等元素的原始顺序：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Task <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name     <span class="type">string</span></span><br><span class="line">    Priority <span class="type">int</span></span><br><span class="line">    Order    <span class="type">int</span>  <span class="comment">// 原始顺序</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">tasks := []Task&#123;</span><br><span class="line">    &#123;<span class="string">&quot;任务A&quot;</span>, <span class="number">2</span>, <span class="number">1</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;任务B&quot;</span>, <span class="number">1</span>, <span class="number">2</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;任务C&quot;</span>, <span class="number">2</span>, <span class="number">3</span>&#125;,  <span class="comment">// 和任务A优先级相同</span></span><br><span class="line">    &#123;<span class="string">&quot;任务D&quot;</span>, <span class="number">1</span>, <span class="number">4</span>&#125;,  <span class="comment">// 和任务B优先级相同</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用 sort.Slice（不稳定）</span></span><br><span class="line">sort.Slice(tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> tasks[i].Priority &lt; tasks[j].Priority</span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(<span class="string">&quot;sort.Slice:&quot;</span>)</span><br><span class="line"><span class="keyword">for</span> _, t := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  %s (优先级:%d, 原序:%d)\n&quot;</span>, t.Name, t.Priority, t.Order)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 任务B 和 任务D 的顺序可能互换</span></span><br><span class="line"><span class="comment">// 任务A 和 任务C 的顺序可能互换</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 重置数据</span></span><br><span class="line">tasks = []Task&#123;</span><br><span class="line">    &#123;<span class="string">&quot;任务A&quot;</span>, <span class="number">2</span>, <span class="number">1</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;任务B&quot;</span>, <span class="number">1</span>, <span class="number">2</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;任务C&quot;</span>, <span class="number">2</span>, <span class="number">3</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;任务D&quot;</span>, <span class="number">1</span>, <span class="number">4</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用 sort.SliceStable（稳定）</span></span><br><span class="line">sort.SliceStable(tasks, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> tasks[i].Priority &lt; tasks[j].Priority</span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(<span class="string">&quot;sort.SliceStable:&quot;</span>)</span><br><span class="line"><span class="keyword">for</span> _, t := <span class="keyword">range</span> tasks &#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  %s (优先级:%d, 原序:%d)\n&quot;</span>, t.Name, t.Priority, t.Order)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 任务B(原序2) 一定在 任务D(原序4) 前面</span></span><br><span class="line"><span class="comment">// 任务A(原序1) 一定在 任务C(原序3) 前面</span></span><br></pre></td></tr></table></figure><p><strong>什么时候用 <code>SliceStable</code></strong>：当你需要按某个字段排序，但相同字段值的元素要保持原来的顺序时。比如&quot;先按优先级排，同优先级的按创建时间排&quot;。</p><hr><h2 id="5-sort-Interface——Go-的排序哲学">5. <code>sort.Interface</code>——Go 的排序哲学</h2><h3 id="5-1-什么是-sort-Interface">5.1 什么是 <code>sort.Interface</code></h3><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">type</span> Interface <span class="keyword">interface</span> &#123;</span><br><span class="line">    Len() <span class="type">int</span></span><br><span class="line">    Less(i, j <span class="type">int</span>) <span class="type">bool</span></span><br><span class="line">    Swap(i, j <span class="type">int</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只要你实现了这三个方法，就可以用 <code>sort.Sort</code> 排序。这是 Go 的经典做法——用接口表达能力。</p><h3 id="5-2-用-sort-Interface-排序自定义类型">5.2 用 <code>sort.Interface</code> 排序自定义类型</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义一个类型，实现 sort.Interface</span></span><br><span class="line"><span class="keyword">type</span> ByAge []Person</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByAge)</span></span> Len() <span class="type">int</span>           &#123; <span class="keyword">return</span> <span class="built_in">len</span>(p) &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByAge)</span></span> Less(i, j <span class="type">int</span>) <span class="type">bool</span> &#123; <span class="keyword">return</span> p[i].Age &lt; p[j].Age &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByAge)</span></span> Swap(i, j <span class="type">int</span>)      &#123; p[i], p[j] = p[j], p[i] &#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">    people := []Person&#123;</span><br><span class="line">        &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">30</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;Bob&quot;</span>, <span class="number">25</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;Charlie&quot;</span>, <span class="number">35</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    sort.Sort(ByAge(people))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, p := <span class="keyword">range</span> people &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;%s: %d岁\n&quot;</span>, p.Name, p.Age)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Bob: 25岁</span></span><br><span class="line">    <span class="comment">// Alice: 30岁</span></span><br><span class="line">    <span class="comment">// Charlie: 35岁</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>思路</strong>：定义一个新类型 <code>ByAge</code>（底层是 <code>[]Person</code>），在这个类型上实现排序需要的三个方法。排序时把 <code>[]Person</code> 转换为 <code>ByAge</code>。</p><h3 id="5-3-多种排序方式">5.3 多种排序方式</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> ByName []Person</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByName)</span></span> Len() <span class="type">int</span>           &#123; <span class="keyword">return</span> <span class="built_in">len</span>(p) &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByName)</span></span> Less(i, j <span class="type">int</span>) <span class="type">bool</span> &#123; <span class="keyword">return</span> p[i].Name &lt; p[j].Name &#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p ByName)</span></span> Swap(i, j <span class="type">int</span>)      &#123; p[i], p[j] = p[j], p[i] &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用</span></span><br><span class="line">sort.Sort(ByAge(people))   <span class="comment">// 按年龄排序</span></span><br><span class="line">sort.Sort(ByName(people))  <span class="comment">// 按姓名排序</span></span><br></pre></td></tr></table></figure><p>每种排序方式定义一个类型，各管各的，互不干扰。</p><h3 id="5-4-降序排序">5.4 降序排序</h3><p><code>sort.Reverse</code> 包装一个 <code>sort.Interface</code>，反转 <code>Less</code> 的结果：</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">sort.Sort(sort.Reverse(ByAge(people)))  <span class="comment">// 按年龄降序</span></span><br></pre></td></tr></table></figure><hr><h2 id="6-查找操作">6. 查找操作</h2><h3 id="6-1-sort-SearchInts：在有序切片中查找">6.1 <code>sort.SearchInts</code>：在有序切片中查找</h3><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">nums := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">7</span>, <span class="number">9</span>, <span class="number">11</span>, <span class="number">13</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 查找 7 的位置</span></span><br><span class="line">idx := sort.SearchInts(nums, <span class="number">7</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 3</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 查找不存在的元素</span></span><br><span class="line">idx = sort.SearchInts(nums, <span class="number">6</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 3（返回应该插入的位置，即第一个 &gt;= 6 的位置）</span></span><br></pre></td></tr></table></figure><p><strong>注意</strong>：<code>SearchInts</code> 假设切片已经是排序好的。如果没排序，结果无意义。</p><h3 id="6-2-sort-SearchStrings-和-sort-SearchFloat64s">6.2 <code>sort.SearchStrings</code> 和 <code>sort.SearchFloat64s</code></h3><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">names := []<span class="type">string</span>&#123;<span class="string">&quot;Alice&quot;</span>, <span class="string">&quot;Bob&quot;</span>, <span class="string">&quot;Charlie&quot;</span>, <span class="string">&quot;David&quot;</span>&#125;</span><br><span class="line">idx := sort.SearchStrings(names, <span class="string">&quot;Charlie&quot;</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line">idx = sort.SearchStrings(names, <span class="string">&quot;Cathy&quot;</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 2（插入到 Charlie 前面）</span></span><br></pre></td></tr></table></figure><h3 id="6-3-sort-Search：通用二分查找">6.3 <code>sort.Search</code>：通用二分查找</h3><p><code>sort.Search</code> 是更通用的版本，你提供一个判断函数：</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">nums := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">7</span>, <span class="number">9</span>, <span class="number">11</span>, <span class="number">13</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 找第一个 &gt;= 6 的数的位置</span></span><br><span class="line">idx := sort.Search(<span class="built_in">len</span>(nums), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &gt;= <span class="number">6</span></span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(idx)        <span class="comment">// 3</span></span><br><span class="line">fmt.Println(nums[idx])  <span class="comment">// 7</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 找第一个 &gt; 5 的数</span></span><br><span class="line">idx = sort.Search(<span class="built_in">len</span>(nums), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &gt; <span class="number">5</span></span><br><span class="line">&#125;)</span><br><span class="line">fmt.Println(idx)        <span class="comment">// 3</span></span><br><span class="line">fmt.Println(nums[idx])  <span class="comment">// 7</span></span><br></pre></td></tr></table></figure><h3 id="6-4-用二分查找做范围查询">6.4 用二分查找做范围查询</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 找第一个 &gt; 4 且 &lt; 10 的数</span></span><br><span class="line">nums := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">7</span>, <span class="number">9</span>, <span class="number">11</span>, <span class="number">13</span>&#125;</span><br><span class="line"></span><br><span class="line">start := sort.Search(<span class="built_in">len</span>(nums), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &gt; <span class="number">4</span></span><br><span class="line">&#125;)</span><br><span class="line">end := sort.Search(<span class="built_in">len</span>(nums), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &gt;= <span class="number">10</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">fmt.Println(nums[start:end])  <span class="comment">// [5 7 9]</span></span><br><span class="line"><span class="comment">// 所有 &gt; 4 且 &lt; 10 的数</span></span><br></pre></td></tr></table></figure><h3 id="6-5-sort-IsSorted：判断是否已排序">6.5 <code>sort.IsSorted</code>：判断是否已排序</h3><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">nums := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">4</span>&#125;</span><br><span class="line">fmt.Println(sort.IsSorted(sort.IntSlice(nums)))  <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line">nums = []<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>&#125;</span><br><span class="line">fmt.Println(sort.IsSorted(sort.IntSlice(nums)))  <span class="comment">// true</span></span><br></pre></td></tr></table></figure><hr><h2 id="7-集合操作——用切片和-map-实现">7. 集合操作——用切片和 map 实现</h2><p>Go 没有内置的集合类型。但你可以用切片和 map 来实现常见的集合操作。</p><h3 id="7-1-去重">7.1 去重</h3><p><strong>方法一：用 map 去重（无序）</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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">dedup</span><span class="params">(items []<span class="type">string</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    result := []<span class="type">string</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[item] &#123;</span><br><span class="line">            seen[item] = <span class="literal">true</span></span><br><span class="line">            result = <span class="built_in">append</span>(result, item)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    tags := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;json&quot;</span>, <span class="string">&quot;go&quot;</span>, <span class="string">&quot;http&quot;</span>, <span class="string">&quot;json&quot;</span>, <span class="string">&quot;go&quot;</span>&#125;</span><br><span class="line">    unique := dedup(tags)</span><br><span class="line">    fmt.Println(unique)  <span class="comment">// [go json http]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>方法二：用 map 去重并保持顺序</strong></p><p>上面的方法已经保持了顺序（因为是按遍历顺序添加的）。注意遍历 map 的顺序是随机的，但我们这里是按切片顺序遍历的，所以结果是稳定的。</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><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">dedupInt</span><span class="params">(nums []<span class="type">int</span>)</span></span> []<span class="type">int</span> &#123;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">int</span>]<span class="type">bool</span>)</span><br><span class="line">    result := []<span class="type">int</span>&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, n := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[n] &#123;</span><br><span class="line">            seen[n] = <span class="literal">true</span></span><br><span class="line">            result = <span class="built_in">append</span>(result, n)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-2-交集">7.2 交集</h3><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="function"><span class="keyword">func</span> <span class="title">intersect</span><span class="params">(a, b []<span class="type">string</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line">    <span class="comment">// 用 map 记录 b 中的元素</span></span><br><span class="line">    setB := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> b &#123;</span><br><span class="line">        setB[item] = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result := []<span class="type">string</span>&#123;&#125;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>) <span class="comment">// 防止结果中出现重复</span></span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> a &#123;</span><br><span class="line">        <span class="keyword">if</span> setB[item] &amp;&amp; !seen[item] &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, item)</span><br><span class="line">            seen[item] = <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    a := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;java&quot;</span>, <span class="string">&quot;python&quot;</span>, <span class="string">&quot;go&quot;</span>, <span class="string">&quot;rust&quot;</span>&#125;</span><br><span class="line">    b := []<span class="type">string</span>&#123;<span class="string">&quot;python&quot;</span>, <span class="string">&quot;go&quot;</span>, <span class="string">&quot;c++&quot;</span>, <span class="string">&quot;javascript&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    common := intersect(a, b)</span><br><span class="line">    fmt.Println(common)  <span class="comment">// [go python]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-3-并集">7.3 并集</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">union</span><span class="params">(a, b []<span class="type">string</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    result := []<span class="type">string</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> a &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[item] &#123;</span><br><span class="line">            seen[item] = <span class="literal">true</span></span><br><span class="line">            result = <span class="built_in">append</span>(result, item)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> b &#123;</span><br><span class="line">        <span class="keyword">if</span> !seen[item] &#123;</span><br><span class="line">            seen[item] = <span class="literal">true</span></span><br><span class="line">            result = <span class="built_in">append</span>(result, item)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    a := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;java&quot;</span>, <span class="string">&quot;python&quot;</span>&#125;</span><br><span class="line">    b := []<span class="type">string</span>&#123;<span class="string">&quot;python&quot;</span>, <span class="string">&quot;go&quot;</span>, <span class="string">&quot;rust&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    all := union(a, b)</span><br><span class="line">    fmt.Println(all)  <span class="comment">// [go java python rust]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-4-差集（A-B）">7.4 差集（A - B）</h3><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">difference</span><span class="params">(a, b []<span class="type">string</span>)</span></span> []<span class="type">string</span> &#123;</span><br><span class="line">    setB := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> b &#123;</span><br><span class="line">        setB[item] = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    result := []<span class="type">string</span>&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> a &#123;</span><br><span class="line">        <span class="keyword">if</span> !setB[item] &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, item)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    a := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;java&quot;</span>, <span class="string">&quot;python&quot;</span>, <span class="string">&quot;rust&quot;</span>&#125;</span><br><span class="line">    b := []<span class="type">string</span>&#123;<span class="string">&quot;python&quot;</span>, <span class="string">&quot;go&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    diff := difference(a, b)</span><br><span class="line">    fmt.Println(diff)  <span class="comment">// [java rust]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-5-判断元素是否存在">7.5 判断元素是否存在</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 用 map 做快速查找</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">contains</span><span class="params">(items []<span class="type">string</span>, target <span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    set := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        set[item] = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> set[target]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果只需要检查一次，用线性搜索就够了</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">containsLinear</span><span class="params">(items []<span class="type">string</span>, target <span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> items &#123;</span><br><span class="line">        <span class="keyword">if</span> item == target &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果需要多次检查，用 map 更高效（O(1) vs O(n)）。如果只检查一次，线性搜索就够了，不用建 map。</p><h3 id="7-6-子集判断">7.6 子集判断</h3><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="function"><span class="keyword">func</span> <span class="title">isSubset</span><span class="params">(a, b []<span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    setB := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> b &#123;</span><br><span class="line">        setB[item] = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> _, item := <span class="keyword">range</span> a &#123;</span><br><span class="line">        <span class="keyword">if</span> !setB[item] &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</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">    a := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;python&quot;</span>&#125;</span><br><span class="line">    b := []<span class="type">string</span>&#123;<span class="string">&quot;go&quot;</span>, <span class="string">&quot;java&quot;</span>, <span class="string">&quot;python&quot;</span>, <span class="string">&quot;rust&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    fmt.Println(isSubset(a, b))  <span class="comment">// true（a 是 b 的子集）</span></span><br><span class="line">    fmt.Println(isSubset(b, a))  <span class="comment">// false（b 不是 a 的子集）</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-7-过滤">7.7 过滤</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 过滤出满足条件的元素</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">filter</span><span class="params">(nums []<span class="type">int</span>, predicate <span class="keyword">func</span>(<span class="type">int</span>)</span></span> <span class="type">bool</span>) []<span class="type">int</span> &#123;</span><br><span class="line">    result := []<span class="type">int</span>&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, n := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        <span class="keyword">if</span> predicate(n) &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, n)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    nums := []<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>, <span class="number">7</span>, <span class="number">8</span>, <span class="number">9</span>, <span class="number">10</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 过滤出偶数</span></span><br><span class="line">    evens := filter(nums, <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> n%<span class="number">2</span> == <span class="number">0</span></span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(evens)  <span class="comment">// [2 4 6 8 10]</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 过滤出大于5的数</span></span><br><span class="line">    big := filter(nums, <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> n &gt; <span class="number">5</span></span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(big)  <span class="comment">// [6 7 8 9 10]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-8-映射（map-操作）">7.8 映射（map 操作）</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对每个元素做变换</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">mapFunc</span><span class="params">(nums []<span class="type">int</span>, transform <span class="keyword">func</span>(<span class="type">int</span>)</span></span> <span class="type">int</span>) []<span class="type">int</span> &#123;</span><br><span class="line">    result := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="built_in">len</span>(nums))</span><br><span class="line">    <span class="keyword">for</span> i, n := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        result[i] = transform(n)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    nums := []<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>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 每个数乘以 2</span></span><br><span class="line">    doubled := mapFunc(nums, <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> n * <span class="number">2</span></span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(doubled)  <span class="comment">// [2 4 6 8 10]</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 每个数的平方</span></span><br><span class="line">    squared := mapFunc(nums, <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> n * n</span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(squared)  <span class="comment">// [1 4 9 16 25]</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-9-归约（reduce）">7.9 归约（reduce）</h3><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="function"><span class="keyword">func</span> <span class="title">reduce</span><span class="params">(nums []<span class="type">int</span>, initial <span class="type">int</span>, combine <span class="keyword">func</span>(<span class="type">int</span>, <span class="type">int</span>)</span></span> <span class="type">int</span>) <span class="type">int</span> &#123;</span><br><span class="line">    result := initial</span><br><span class="line">    <span class="keyword">for</span> _, n := <span class="keyword">range</span> nums &#123;</span><br><span class="line">        result = combine(result, n)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</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">    nums := []<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>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 求和</span></span><br><span class="line">    sum := reduce(nums, <span class="number">0</span>, <span class="function"><span class="keyword">func</span><span class="params">(acc, n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> acc + n</span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(sum)  <span class="comment">// 15</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 求积</span></span><br><span class="line">    product := reduce(nums, <span class="number">1</span>, <span class="function"><span class="keyword">func</span><span class="params">(acc, n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> acc * n</span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(product)  <span class="comment">// 120</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 求最大值</span></span><br><span class="line">    max := reduce(nums, nums[<span class="number">0</span>], <span class="function"><span class="keyword">func</span><span class="params">(acc, n <span class="type">int</span>)</span></span> <span class="type">int</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> n &gt; acc &#123;</span><br><span class="line">            <span class="keyword">return</span> n</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> acc</span><br><span class="line">    &#125;)</span><br><span class="line">    fmt.Println(max)  <span class="comment">// 5</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="8-实战综合示例">8. 实战综合示例</h2><h3 id="8-1-商品排序与筛选系统">8.1 商品排序与筛选系统</h3><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><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></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;sort&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Product <span class="keyword">struct</span> &#123;</span><br><span class="line">    ID       <span class="type">int</span></span><br><span class="line">    Name     <span class="type">string</span></span><br><span class="line">    Price    <span class="type">float64</span></span><br><span class="line">    Sales    <span class="type">int</span></span><br><span class="line">    Rating   <span class="type">float64</span></span><br><span class="line">    Category <span class="type">string</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">    products := []Product&#123;</span><br><span class="line">        &#123;<span class="number">1</span>, <span class="string">&quot;Go 编程指南&quot;</span>, <span class="number">59.9</span>, <span class="number">1200</span>, <span class="number">4.8</span>, <span class="string">&quot;图书&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="number">2</span>, <span class="string">&quot;机械键盘&quot;</span>, <span class="number">299.0</span>, <span class="number">800</span>, <span class="number">4.5</span>, <span class="string">&quot;外设&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="number">3</span>, <span class="string">&quot;显示器&quot;</span>, <span class="number">1999.0</span>, <span class="number">300</span>, <span class="number">4.7</span>, <span class="string">&quot;外设&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="number">4</span>, <span class="string">&quot;Python 入门&quot;</span>, <span class="number">49.9</span>, <span class="number">2000</span>, <span class="number">4.6</span>, <span class="string">&quot;图书&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="number">5</span>, <span class="string">&quot;鼠标&quot;</span>, <span class="number">99.0</span>, <span class="number">1500</span>, <span class="number">4.3</span>, <span class="string">&quot;外设&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="number">6</span>, <span class="string">&quot;Java 核心技术&quot;</span>, <span class="number">79.9</span>, <span class="number">900</span>, <span class="number">4.9</span>, <span class="string">&quot;图书&quot;</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 按价格升序</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 按价格升序 ===&quot;</span>)</span><br><span class="line">    sort.Slice(products, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> products[i].Price &lt; products[j].Price</span><br><span class="line">    &#125;)</span><br><span class="line">    printProducts(products)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 按销量降序</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 按销量降序 ===&quot;</span>)</span><br><span class="line">    sort.Slice(products, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> products[i].Sales &gt; products[j].Sales</span><br><span class="line">    &#125;)</span><br><span class="line">    printProducts(products)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 按评分降序，评分相同按销量降序</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 按评分降序（同分按销量）===&quot;</span>)</span><br><span class="line">    sort.Slice(products, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> products[i].Rating != products[j].Rating &#123;</span><br><span class="line">            <span class="keyword">return</span> products[i].Rating &gt; products[j].Rating</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> products[i].Sales &gt; products[j].Sales</span><br><span class="line">    &#125;)</span><br><span class="line">    printProducts(products)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 筛选外设类商品，再按价格排序</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 外设类商品（按价格）===&quot;</span>)</span><br><span class="line">    peripherals := filterProducts(products, <span class="function"><span class="keyword">func</span><span class="params">(p Product)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> p.Category == <span class="string">&quot;外设&quot;</span></span><br><span class="line">    &#125;)</span><br><span class="line">    sort.Slice(peripherals, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> peripherals[i].Price &lt; peripherals[j].Price</span><br><span class="line">    &#125;)</span><br><span class="line">    printProducts(peripherals)</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">printProducts</span><span class="params">(products []Product)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> _, p := <span class="keyword">range</span> products &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: ¥%.1f, 销量:%d, 评分:%.1f\n&quot;</span>,</span><br><span class="line">            p.Name, p.Price, p.Sales, p.Rating)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Println()</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">filterProducts</span><span class="params">(products []Product, predicate <span class="keyword">func</span>(Product)</span></span> <span class="type">bool</span>) []Product &#123;</span><br><span class="line">    result := []Product&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, p := <span class="keyword">range</span> products &#123;</span><br><span class="line">        <span class="keyword">if</span> predicate(p) &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, p)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-2-数据去重与统计">8.2 数据去重与统计</h3><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><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</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;sort&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> LogEntry <span class="keyword">struct</span> &#123;</span><br><span class="line">    Timestamp <span class="type">string</span></span><br><span class="line">    Level     <span class="type">string</span></span><br><span class="line">    Message   <span class="type">string</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">    logs := []LogEntry&#123;</span><br><span class="line">        &#123;<span class="string">&quot;10:00:01&quot;</span>, <span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;服务启动&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;10:00:02&quot;</span>, <span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;连接数据库&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;10:00:03&quot;</span>, <span class="string">&quot;WARN&quot;</span>, <span class="string">&quot;数据库响应慢&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;10:00:04&quot;</span>, <span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;服务启动&quot;</span>&#125;,       <span class="comment">// 重复消息</span></span><br><span class="line">        &#123;<span class="string">&quot;10:00:05&quot;</span>, <span class="string">&quot;ERROR&quot;</span>, <span class="string">&quot;数据库连接失败&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;10:00:06&quot;</span>, <span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;重试连接&quot;</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;10:00:07&quot;</span>, <span class="string">&quot;WARN&quot;</span>, <span class="string">&quot;数据库响应慢&quot;</span>&#125;,   <span class="comment">// 重复消息</span></span><br><span class="line">        &#123;<span class="string">&quot;10:00:08&quot;</span>, <span class="string">&quot;INFO&quot;</span>, <span class="string">&quot;连接成功&quot;</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 统计各级别的日志数量</span></span><br><span class="line">    levelCount := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>)</span><br><span class="line">    <span class="keyword">for</span> _, log := <span class="keyword">range</span> logs &#123;</span><br><span class="line">        levelCount[log.Level]++</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 按数量排序输出</span></span><br><span class="line">    <span class="keyword">type</span> KV <span class="keyword">struct</span> &#123;</span><br><span class="line">        Key   <span class="type">string</span></span><br><span class="line">        Value <span class="type">int</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">var</span> sorted []KV</span><br><span class="line">    <span class="keyword">for</span> k, v := <span class="keyword">range</span> levelCount &#123;</span><br><span class="line">        sorted = <span class="built_in">append</span>(sorted, KV&#123;k, v&#125;)</span><br><span class="line">    &#125;</span><br><span class="line">    sort.Slice(sorted, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> sorted[i].Value &gt; sorted[j].Value</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 日志级别统计 ===&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, kv := <span class="keyword">range</span> sorted &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  %s: %d 条\n&quot;</span>, kv.Key, kv.Value)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 去重（按消息内容去重）</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n=== 去重后的日志 ===&quot;</span>)</span><br><span class="line">    seen := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">bool</span>)</span><br><span class="line">    unique := []LogEntry&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> _, log := <span class="keyword">range</span> logs &#123;</span><br><span class="line">        key := log.Level + <span class="string">&quot;:&quot;</span> + log.Message</span><br><span class="line">        <span class="keyword">if</span> !seen[key] &#123;</span><br><span class="line">            seen[key] = <span class="literal">true</span></span><br><span class="line">            unique = <span class="built_in">append</span>(unique, log)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> _, log := <span class="keyword">range</span> unique &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;  [%s] %s %s\n&quot;</span>, log.Timestamp, log.Level, log.Message)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-3-成绩排名与分析">8.3 成绩排名与分析</h3><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><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><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</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;sort&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> ScoreRecord <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name  <span class="type">string</span></span><br><span class="line">    Math  <span class="type">int</span></span><br><span class="line">    English <span class="type">int</span></span><br><span class="line">    Chinese <span class="type">int</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">(s ScoreRecord)</span></span> Total() <span class="type">int</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> s.Math + s.English + s.Chinese</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">(s ScoreRecord)</span></span> Average() <span class="type">float64</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="type">float64</span>(s.Total()) / <span class="number">3.0</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">    records := []ScoreRecord&#123;</span><br><span class="line">        &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">95</span>, <span class="number">88</span>, <span class="number">92</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;Bob&quot;</span>, <span class="number">78</span>, <span class="number">95</span>, <span class="number">85</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;Charlie&quot;</span>, <span class="number">88</span>, <span class="number">76</span>, <span class="number">95</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;David&quot;</span>, <span class="number">92</span>, <span class="number">91</span>, <span class="number">88</span>&#125;,</span><br><span class="line">        &#123;<span class="string">&quot;Eve&quot;</span>, <span class="number">65</span>, <span class="number">72</span>, <span class="number">78</span>&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 按总分降序排列</span></span><br><span class="line">    sort.Slice(records, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> records[i].Total() &gt; records[j].Total()</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">&quot;=== 总分排名 ===&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> rank, r := <span class="keyword">range</span> records &#123;</span><br><span class="line">        fmt.Printf(<span class="string">&quot;第%d名: %s, 总分:%d, 平均分:%.1f\n&quot;</span>,</span><br><span class="line">            rank+<span class="number">1</span>, r.Name, r.Total(), r.Average())</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 找出各科最高分</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n=== 各科最高分 ===&quot;</span>)</span><br><span class="line">    bestMath, bestEnglish, bestChinese := <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">    mathName, englishName, chineseName := <span class="string">&quot;&quot;</span>, <span class="string">&quot;&quot;</span>, <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> records &#123;</span><br><span class="line">        <span class="keyword">if</span> r.Math &gt; bestMath &#123;</span><br><span class="line">            bestMath = r.Math</span><br><span class="line">            mathName = r.Name</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> r.English &gt; bestEnglish &#123;</span><br><span class="line">            bestEnglish = r.English</span><br><span class="line">            englishName = r.Name</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> r.Chinese &gt; bestChinese &#123;</span><br><span class="line">            bestChinese = r.Chinese</span><br><span class="line">            chineseName = r.Name</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">&quot;  数学最高: %s (%d分)\n&quot;</span>, mathName, bestMath)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  英语最高: %s (%d分)\n&quot;</span>, englishName, bestEnglish)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  语文最高: %s (%d分)\n&quot;</span>, chineseName, bestChinese)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算各科平均分</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n=== 各科平均分 ===&quot;</span>)</span><br><span class="line">    mathSum, englishSum, chineseSum := <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> records &#123;</span><br><span class="line">        mathSum += r.Math</span><br><span class="line">        englishSum += r.English</span><br><span class="line">        chineseSum += r.Chinese</span><br><span class="line">    &#125;</span><br><span class="line">    n := <span class="type">float64</span>(<span class="built_in">len</span>(records))</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  数学平均: %.1f\n&quot;</span>, <span class="type">float64</span>(mathSum)/n)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  英语平均: %.1f\n&quot;</span>, <span class="type">float64</span>(englishSum)/n)</span><br><span class="line">    fmt.Printf(<span class="string">&quot;  语文平均: %.1f\n&quot;</span>, <span class="type">float64</span>(chineseSum)/n)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 筛选不及格的学生</span></span><br><span class="line">    fmt.Println(<span class="string">&quot;\n=== 不及格学生 ===&quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> records &#123;</span><br><span class="line">        failed := []<span class="type">string</span>&#123;&#125;</span><br><span class="line">        <span class="keyword">if</span> r.Math &lt; <span class="number">60</span> &#123;</span><br><span class="line">            failed = <span class="built_in">append</span>(failed, <span class="string">&quot;数学&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> r.English &lt; <span class="number">60</span> &#123;</span><br><span class="line">            failed = <span class="built_in">append</span>(failed, <span class="string">&quot;英语&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> r.Chinese &lt; <span class="number">60</span> &#123;</span><br><span class="line">            failed = <span class="built_in">append</span>(failed, <span class="string">&quot;语文&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(failed) &gt; <span class="number">0</span> &#123;</span><br><span class="line">            fmt.Printf(<span class="string">&quot;  %s: %s 不及格\n&quot;</span>, r.Name,</span><br><span class="line">                fmt.Sprint(failed))</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><hr><h2 id="9-常见坑总结">9. 常见坑总结</h2><h3 id="9-1-sort-Slice-的-less-函数不要修改切片">9.1 <code>sort.Slice</code> 的 <code>less</code> 函数不要修改切片</h3><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="comment">// 错误：在 less 中修改了切片</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    nums[i] = nums[i] * <span class="number">2</span>  <span class="comment">// 不要在比较函数里修改数据！</span></span><br><span class="line">    <span class="keyword">return</span> nums[i] &lt; nums[j]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><code>less</code> 函数只应该比较，不应该修改数据。排序过程中会多次调用 <code>less</code>，在其中修改数据会导致不可预期的结果。</p><h3 id="9-2-二分查找必须在已排序的切片上使用">9.2 二分查找必须在已排序的切片上使用</h3><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">nums := []<span class="type">int</span>&#123;<span class="number">5</span>, <span class="number">3</span>, <span class="number">1</span>, <span class="number">4</span>, <span class="number">2</span>&#125;</span><br><span class="line">idx := sort.SearchInts(nums, <span class="number">3</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 4（错误的结果！）</span></span><br></pre></td></tr></table></figure><p><code>SearchInts</code>、<code>SearchStrings</code>、<code>Search</code> 都假设切片已排序。没排序的切片上使用会得到无意义的结果。</p><h3 id="9-3-二分查找找不到时的返回值">9.3 二分查找找不到时的返回值</h3><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">nums := []<span class="type">int</span>&#123;<span class="number">1</span>, <span class="number">3</span>, <span class="number">5</span>, <span class="number">7</span>, <span class="number">9</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 查找 6（不存在）</span></span><br><span class="line">idx := sort.SearchInts(nums, <span class="number">6</span>)</span><br><span class="line">fmt.Println(idx)  <span class="comment">// 3（应该插入的位置）</span></span><br><span class="line"><span class="comment">// 不是 -1！返回的是第一个 &gt;= 6 的元素的位置</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 要判断是否真的找到了，需要自己检查</span></span><br><span class="line"><span class="keyword">if</span> idx &lt; <span class="built_in">len</span>(nums) &amp;&amp; nums[idx] == <span class="number">6</span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;找到了&quot;</span>)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">&quot;没找到&quot;</span>)  <span class="comment">// 输出这个</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-4-整数溢出导致排序错误">9.4 整数溢出导致排序错误</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 非常大的数做减法会溢出</span></span><br><span class="line">nums := []<span class="type">int</span>&#123;<span class="number">2147483647</span>, <span class="number">-2147483648</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 错误做法：用减法比较（可能溢出）</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i]-nums[j] &lt; <span class="number">0</span>  <span class="comment">// 溢出！结果不可预测</span></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确做法：直接比较</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nums[i] &lt; nums[j]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="9-5-sort-Slice-的-less-函数不能有副作用">9.5 <code>sort.Slice</code> 的 <code>less</code> 函数不能有副作用</h3><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"><span class="comment">// 错误：在 less 中打印信息</span></span><br><span class="line">count := <span class="number">0</span></span><br><span class="line">sort.Slice(nums, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    count++</span><br><span class="line">    fmt.Println(<span class="string">&quot;第&quot;</span>, count, <span class="string">&quot;次比较&quot;</span>)  <span class="comment">// 副作用！</span></span><br><span class="line">    <span class="keyword">return</span> nums[i] &lt; nums[j]</span><br><span class="line">&#125;)</span><br><span class="line"><span class="comment">// 排序过程中 less 会被调用多次，次数不确定</span></span><br><span class="line"><span class="comment">// 不要在 less 中做计数、日志等操作</span></span><br></pre></td></tr></table></figure><h3 id="9-6-map-去重时注意值类型的比较">9.6 map 去重时注意值类型的比较</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对结构体切片去重</span></span><br><span class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">people := []Person&#123;</span><br><span class="line">    &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">25</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Bob&quot;</span>, <span class="number">30</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">25</span>&#125;,  <span class="comment">// 重复</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用 map 去重</span></span><br><span class="line">seen := <span class="built_in">make</span>(<span class="keyword">map</span>[Person]<span class="type">bool</span>)  <span class="comment">// 用结构体做 map 键是可以的</span></span><br><span class="line">unique := []Person&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> _, p := <span class="keyword">range</span> people &#123;</span><br><span class="line">    <span class="keyword">if</span> !seen[p] &#123;</span><br><span class="line">        seen[p] = <span class="literal">true</span></span><br><span class="line">        unique = <span class="built_in">append</span>(unique, p)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(unique)  <span class="comment">// [&#123;Alice 25&#125; &#123;Bob 30&#125;]</span></span><br></pre></td></tr></table></figure><p>结构体可以做 map 的键，前提是所有字段都是可比较的类型（不能有 slice、map、function 字段）。</p><h3 id="9-7-浮点数排序中的-NaN">9.7 浮点数排序中的 NaN</h3><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">nums := []<span class="type">float64</span>&#123;<span class="number">1.0</span>, math.NaN(), <span class="number">3.0</span>, <span class="number">2.0</span>&#125;</span><br><span class="line">sort.Float64s(nums)</span><br><span class="line">fmt.Println(nums)  <span class="comment">// NaN 的位置不确定</span></span><br></pre></td></tr></table></figure><p><code>NaN</code> 和任何数比较（包括自己）都是 <code>false</code>，所以包含 <code>NaN</code> 的切片排序结果不确定。如果有 <code>NaN</code>，需要特殊处理。</p><h3 id="9-8-对-nil-切片排序">9.8 对 nil 切片排序</h3><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> nums []<span class="type">int</span>  <span class="comment">// nil 切片</span></span><br><span class="line">sort.Ints(nums)</span><br><span class="line">fmt.Println(nums)  <span class="comment">// nil（不会 panic）</span></span><br></pre></td></tr></table></figure><p>对 nil 切片或空切片排序是安全的，不会 panic。</p><hr><h2 id="10-本课练习">10. 本课练习</h2><h3 id="练习-1：学生成绩排序">练习 1：学生成绩排序</h3><p>要求：</p><ul><li>定义 <code>Student</code> 结构体（<code>Name</code>、<code>Score</code>、<code>Class</code>）</li><li>按成绩降序排列输出</li><li>成绩相同的按姓名升序排列</li></ul><hr><h3 id="练习-2：商品筛选与排序">练习 2：商品筛选与排序</h3><p>要求：</p><ul><li>定义 <code>Product</code> 结构体（<code>Name</code>、<code>Price</code>、<code>Category</code>、<code>InStock</code>）</li><li>筛选出库存中的商品</li><li>按类别分组，每个类别内按价格升序排列</li></ul><hr><h3 id="练习-3：集合操作库">练习 3：集合操作库</h3><p>要求：</p><ul><li>实现 <code>intersect</code>（交集）、<code>union</code>（并集）、<code>difference</code>（差集）</li><li>支持 <code>int</code> 类型</li><li>返回的结果中不包含重复元素</li></ul><hr><h3 id="练习-4：日志分析">练习 4：日志分析</h3><p>要求：</p><ul><li>给定一组日志条目（<code>Timestamp</code>、<code>Level</code>、<code>Message</code>）</li><li>按时间排序</li><li>统计各日志级别的数量</li><li>找出出现次数最多的错误消息</li></ul><hr><h3 id="练习-5：实现排序算法">练习 5：实现排序算法</h3><p>要求：</p><ul><li>不用 <code>sort</code> 包，自己实现冒泡排序</li><li>用 <code>sort.Slice</code> 实现同样的排序</li><li>比较两种方式的代码量差异</li></ul><hr><h2 id="11-自测题">11. 自测题</h2><h3 id="11-1-概念题">11.1 概念题</h3><ol><li><code>sort.Slice</code> 和 <code>sort.SliceStable</code> 有什么区别？</li><li><code>sort.Sort</code> 需要实现哪三个方法？</li><li><code>sort.SearchInts</code> 返回 <code>6</code> 但切片中没有 <code>6</code>，这是什么意思？</li><li>对 nil 切片调用 <code>sort.Ints</code> 会 panic 吗？</li><li><code>sort.Reverse</code> 的工作原理是什么？</li><li>在 <code>sort.Slice</code> 的比较函数中修改切片数据会怎样？</li><li>用 map 去重时，如何保持元素的原始顺序？</li><li>Go 有没有内置的集合类型？用什么代替？</li></ol><h3 id="11-2-代码阅读题">11.2 代码阅读题</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="type">string</span></span><br><span class="line">    Age  <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">people := []Person&#123;</span><br><span class="line">    &#123;<span class="string">&quot;Alice&quot;</span>, <span class="number">25</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Bob&quot;</span>, <span class="number">25</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;Charlie&quot;</span>, <span class="number">30</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;David&quot;</span>, <span class="number">25</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">sort.SliceStable(people, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> people[i].Age &lt; people[j].Age</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, p := <span class="keyword">range</span> people &#123;</span><br><span class="line">    fmt.Println(p.Name)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details><summary>点击查看答案</summary><figure class="highlight ebnf"><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="attribute">Alice</span></span><br><span class="line"><span class="attribute">Bob</span></span><br><span class="line"><span class="attribute">David</span></span><br><span class="line"><span class="attribute">Charlie</span></span><br></pre></td></tr></table></figure><p>解释：</p><ol><li>按年龄升序排列</li><li>Alice、Bob、David 年龄都是 25，Charlie 是 30</li><li>因为用的是 <code>SliceStable</code>，所以同年龄的人保持原来的顺序：Alice → Bob → David</li><li>Charlie 年龄最大，排在最后</li></ol></details><hr><h2 id="12-本课总结">12. 本课总结</h2><p>这一课你学到了 Go 中排序和集合操作的方法。</p><table><thead><tr><th>场景</th><th>推荐方式</th></tr></thead><tbody><tr><td>基本类型排序</td><td><code>sort.Ints</code> / <code>sort.Float64s</code> / <code>sort.Strings</code></td></tr><tr><td>按自定义规则排序</td><td><code>sort.Slice</code>（最常用）</td></tr><tr><td>保持相等元素顺序</td><td><code>sort.SliceStable</code></td></tr><tr><td>降序排序</td><td><code>sort.Reverse</code> 包装，或 <code>less</code> 中反转比较</td></tr><tr><td>多字段排序</td><td><code>less</code> 函数中先比主字段，再比次字段</td></tr><tr><td>二分查找</td><td><code>sort.SearchInts</code> / <code>sort.Search</code>（必须已排序）</td></tr><tr><td>去重</td><td>map 辅助</td></tr><tr><td>交集/并集/差集</td><td>map 辅助</td></tr><tr><td>过滤</td><td>遍历 + 条件判断</td></tr></tbody></table><p>最重要的三件事：</p><ol><li><strong>排序用 <code>sort.Slice</code>，简单直接——大多数情况不需要 <code>sort.Interface</code></strong></li><li><strong>二分查找必须在已排序的切片上使用——没排序就用会得到错误结果</strong></li><li><strong>Go 没有内置集合，用 map 来实现——<code>map[string]bool</code> 是最常见的&quot;集合&quot;</strong></li></ol><hr><h2 id="13-下一课预告">13. 下一课预告</h2><p>下一课是阶段四的最后一课：<strong>命令行小项目实战</strong>。</p><p>会重点讲：</p><ul><li>整合前面所学（结构体、切片、map、文件、JSON、排序、错误处理）</li><li>完成一个具有实际功能的 CLI 程序</li><li>项目的组织与代码结构</li></ul><p>学完下一课，你就完成了阶段四，具备了用 Go 做真实小项目的完整能力。</p>]]></content>
    
    
    <summary type="html">Go语言系列23</summary>
    
    
    
    <category term="go" scheme="https://yjyrichard.github.io/categories/go/"/>
    
    
    <category term="go" scheme="https://yjyrichard.github.io/tags/go/"/>
    
  </entry>
  
</feed>
