<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>concurrency on Chaney&#39;s MoonBook</title>
    <link>https://chaneyzorn.github.io/tags/concurrency/</link>
    <description>Recent content in concurrency on Chaney&#39;s MoonBook</description>
    <follow_challenge>
        <feedId>94402801171278848</feedId>
        <userId>60932538427591680</userId>
    </follow_challenge>
    <image>
      <title>Chaney&#39;s MoonBook</title>
      <url>https://chaneyzorn.github.io/cm/chaney-cover.jpg</url>
      <link>https://chaneyzorn.github.io/cm/chaney-cover.jpg</link>
    </image>
    <generator>Hugo(0.160.1) -- gohugo.io</generator>
    <language>zh</language>
    <managingEditor>chaneyzorn#gmail#com (ChaneyZorn)</managingEditor>
    <webMaster>chaneyzorn#gmail#com (ChaneyZorn)</webMaster>
    <copyright>Copyright © ChaneyZorn | CC BY-NC-ND 4.0 |</copyright>
    <lastBuildDate>Sun, 26 Jul 2026 10:50:39 +0800</lastBuildDate>
    <atom:link href="https://chaneyzorn.github.io/tags/concurrency/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Go Channel 的边界行为</title>
      <link>https://chaneyzorn.github.io/codes/go-channel-boundary/</link>
      <pubDate>Sun, 26 Jul 2026 10:50:39 +0800</pubDate><author>chaneyzorn#gmail#com (ChaneyZorn)</author>
      <guid>https://chaneyzorn.github.io/codes/go-channel-boundary/</guid>
      <description><![CDATA[<p>Go channel 的边界行为可以从三个状态、两种容量以及一组特殊操作来理解。三个状态是 <code>nil</code>、open 和 closed；两种容量是无缓冲和有缓冲。下面按生命周期、特殊行为和同步语义分层说明。</p>
<h2 id="1-概览">1. 概览</h2>
<h3 id="11-状态与分类">1.1 状态与分类</h3>
<ul>
<li><code>nil</code>：只声明、未通过 <code>make</code> 初始化。</li>
<li>open：已经 <code>make</code>，可以正常通信。</li>
<li>closed：已经调用 <code>close</code>，不再接受发送。</li>
</ul>
<p>再结合容量，channel 可分为无缓冲和有缓冲两类。</p>
<h3 id="12-核心行为矩阵">1.2 核心行为矩阵</h3>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>nil channel</th>
          <th>open channel</th>
          <th>closed channel</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ch &lt;- v</code></td>
          <td>永久阻塞</td>
          <td>根据容量决定是否阻塞</td>
          <td>panic</td>
      </tr>
      <tr>
          <td><code>&lt;-ch</code></td>
          <td>永久阻塞</td>
          <td>根据数据决定是否阻塞</td>
          <td>缓冲区排空后立即返回零值</td>
      </tr>
      <tr>
          <td><code>close(ch)</code></td>
          <td>panic</td>
          <td>成功</td>
          <td>panic</td>
      </tr>
      <tr>
          <td><code>len(ch)</code></td>
          <td>0</td>
          <td>缓冲元素数</td>
          <td>尚未取出的缓冲元素数</td>
      </tr>
      <tr>
          <td><code>cap(ch)</code></td>
          <td>0</td>
          <td>创建时容量</td>
          <td>保持原容量</td>
      </tr>
      <tr>
          <td><code>for range ch</code></td>
          <td>永久阻塞</td>
          <td>持续接收</td>
          <td>排空后退出</td>
      </tr>
  </tbody>
</table>
<h2 id="2-生命周期">2. 生命周期</h2>
<h3 id="21-初始化">2.1 初始化</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">nilCh</span><span class="w"> </span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="w">          </span><span class="c1">// nil channel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">unbuffered</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">buffered</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><table>
  <thead>
      <tr>
          <th>初始化方式</th>
          <th>状态</th>
          <th style="text-align: right"><code>len</code></th>
          <th style="text-align: right"><code>cap</code></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>var ch chan T</code></td>
          <td>nil</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">0</td>
      </tr>
      <tr>
          <td><code>make(chan T)</code></td>
          <td>open、无缓冲</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">0</td>
      </tr>
      <tr>
          <td><code>make(chan T, n)</code></td>
          <td>open、有缓冲</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">n</td>
      </tr>
  </tbody>
</table>
<p>channel 的容量创建后不能改变。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">n</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="c1">// runtime panic: makechan: size out of range</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>len(ch)</code> 的语义只限于返回当前缓冲区中的元素个数，不代表 channel 的完整状态。例如：</p>
<ul>
<li>对无缓冲 channel，<code>len(ch)</code> 恒为 0，但 send/receive 是否阻塞取决于是否有配对的接收/发送方正在等待，不能从 <code>len(ch)</code> 推断。</li>
<li>对有缓冲 channel，<code>len(ch)</code> 只是某一时刻的快照；检查它之后、执行 send/receive 之前，其他 goroutine 可能已经改变了 channel 状态。</li>
</ul>
<p>因此不能用 <code>len(ch)</code> 来判断下一次 send 或 receive 是否会阻塞。</p>
<h3 id="22-nil-channel">2.2 nil channel</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w">   </span><span class="c1">// 永久阻塞</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w"> </span><span class="c1">// 永久阻塞</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// panic</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>nil channel 不等同于 closed channel：</p>
<ul>
<li>nil channel 永远无法完成通信。</li>
<li>closed channel 的接收操作立即完成。</li>
</ul>
<h3 id="23-无缓冲-channel">2.3 无缓冲 channel</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>发送方和接收方必须会合：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>ch &lt;- 42</code> 只有在另一个 goroutine 已经或即将执行接收时才能完成。反方向也一样：接收方会等待发送方。</p>
<p>如果在同一个 goroutine 中发送后才接收：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>发送操作无法完成，程序会出现：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">fatal error: all goroutines are asleep - deadlock!
</span></span></code></pre></td></tr></table>
</div>
</div><p>正确写法需要另一个 goroutine，或使用缓冲区。</p>
<h3 id="24-有缓冲-channel">2.4 有缓冲 channel</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="c1">// 不阻塞</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="c1">// 不阻塞</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="c1">// 缓冲区已满，阻塞</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>发送在缓冲区满时阻塞；接收在缓冲区空时阻塞：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 取出 1，释放一个位置</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>可以将其理解为固定容量队列，但 channel 还包含同步和唤醒语义。</p>
<h3 id="25-关闭-channel">2.5 关闭 channel</h3>
<p>关闭表示“之后不会再发送新值”：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>关闭不会清空缓冲区。接收方先获得剩余数据，然后持续获得元素类型的零值。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 20</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 0</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>区分“发送了零值”和“channel 已排空并关闭”，可使用 comma-ok：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// channel 已关闭且缓冲区已排空</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>缓冲区还有数据时，即使 channel 已关闭，<code>ok</code> 仍为 <code>true</code>：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w"> </span><span class="c1">// 0, true：这是发送过的零值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">  </span><span class="c1">// 0, false：关闭后的零值</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-特殊行为">3. 特殊行为</h2>
<h3 id="31-range-行为">3.1 range 行为</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>等价于反复接收，直到 channel 已关闭且缓冲区排空：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">break</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>如果发送方从不关闭 channel，接收方会在取完最后一个值后继续等待：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 取出 1 后阻塞，因为 ch 没有关闭</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>channel 不需要为了垃圾回收而关闭。只有接收方需要知道“不会再有数据”时才需要关闭。</p>
<h3 id="32-多发送方与多接收方">3.2 多发送方与多接收方</h3>
<p>一个值只会被一个接收者获得。下面的例子中，两个接收者竞争缓冲区里的一个值：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">wg</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">wg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;receiver 1:&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">wg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;receiver 2:&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>可能的输出：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">receiver 1: 42 true
</span></span><span class="line"><span class="cl">receiver 2: 0 false
</span></span></code></pre></td></tr></table>
</div>
</div><p>42 只被其中一个接收者拿到；另一个被 <code>close(ch)</code> 唤醒后得到零值。</p>
<p>关闭 channel 会唤醒所有阻塞的接收者：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">wg</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="mi">3</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">id</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">i</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">wg</span><span class="p">.</span><span class="nf">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;receiver %d ok=%v\n&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 等待接收者都阻塞在 &lt;-ch 上（仅作示例，实际代码不要依赖 sleep）</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">50</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>输出：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">receiver 0 ok=false
</span></span><span class="line"><span class="cl">receiver 1 ok=false
</span></span><span class="line"><span class="cl">receiver 2 ok=false
</span></span></code></pre></td></tr></table>
</div>
</div><p>缓冲数据仍然只会分配给其中某一个接收者；排空后，所有接收者都能立即得到零值和 <code>ok=false</code>。</p>
<p>多个发送方同时发送时，接收顺序与 goroutine 的启动顺序无关，因为实际调度是随机的。因此不能依赖代码中的书写顺序来判断实际接收顺序：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">}()</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>可能先收到 1，也可能先收到 2。</p>
<p>一般约定是：</p>
<blockquote>
<p>创建和关闭 channel 的责任通常属于发送方；存在多个发送方时，应由一个协调者统一关闭。</p>
</blockquote>
<p>不要让多个发送者自行判断并关闭：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">shouldClose</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// 其他发送者可能仍在发送或也准备关闭</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>并发执行 <code>send</code> 与 <code>close</code> 没有安全保证，可能产生数据竞争或 <code>send on closed channel</code>。</p>
<h3 id="33-select-的边界行为">3.3 select 的边界行为</h3>
<p><code>select</code> 同时监听多个 channel 操作，哪个 case 就绪就执行哪个。下面是一个典型结构：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">input</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">use</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">output</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nx">result</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>本节讨论 <code>select</code> 在 nil channel、closed channel 等边界条件下的行为。</p>
<h4 id="331-nil-channel-case-会被禁用">3.3.1 nil channel case 会被禁用</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 永远不会选中</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 执行这里</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>这可以用于动态启用或禁用 case：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">enabled</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">ch</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="332-closed-channel-的接收-case-始终就绪">3.3.2 closed channel 的接收 case 始终就绪</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 立即执行，ok=false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>如果循环中的 select 没有处理 <code>ok</code>，closed channel 可能导致忙循环：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">case</span><span class="w"> </span><span class="nx">v</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nf">process</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span><span class="w"> </span><span class="c1">// ch 关闭后不断处理零值</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>处理方式之一是退出：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>或者将它设为 nil，禁用这个 case：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">v</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">ch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">ok</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nx">ch</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">continue</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="333-向-closed-channel-发送的-case">3.3.3 向 closed channel 发送的 case</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// 如果该 case 被选中，会 panic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">default</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>select</code> 不会替发送方安全地忽略 closed channel。必须通过所有权和同步保证发送时 channel 仍开放。</p>
<h4 id="334-多个-case-同时就绪">3.3.4 多个 case 同时就绪</h4>
<p>如果多个 case 同时可执行，Go 会选择其中一个，不能依赖固定优先级：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">a</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">b</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="335-所有-case-都不可执行">3.3.5 所有 case 都不可执行</h4>
<ul>
<li>有 <code>default</code>：立即执行 <code>default</code>。</li>
<li>没有 <code>default</code>：当前 goroutine 阻塞。</li>
<li>空 <code>select {}</code>：永久阻塞。</li>
</ul>
<h3 id="34-关闭作为广播信号">3.4 关闭作为广播信号</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">start</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kd">struct</span><span class="p">{})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="o">&lt;-</span><span class="nx">start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">work</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="o">&lt;-</span><span class="nx">start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nf">work</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">start</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>关闭 channel 后，所有 <code>&lt;-start</code> 都立即完成，因此适合作为一次性广播信号。</p>
<p>如果只发送一次：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">start</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="kd">struct</span><span class="p">{}{}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>对于无缓冲 channel，一次发送只会唤醒一个接收者。</p>
<p><code>struct{}</code> 不携带实际数据，常用于纯信号 channel：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">chan</span><span class="w"> </span><span class="kd">struct</span><span class="p">{}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="35-channel-方向">3.5 channel 方向</h3>
<p>函数参数可以限制 channel 操作：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">producer</span><span class="p">(</span><span class="nx">out</span><span class="w"> </span><span class="kd">chan</span><span class="o">&lt;-</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">out</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nb">close</span><span class="p">(</span><span class="nx">out</span><span class="p">)</span><span class="w"> </span><span class="c1">// send-only channel 可以关闭</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">consumer</span><span class="p">(</span><span class="nx">in</span><span class="w"> </span><span class="o">&lt;-</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="o">&lt;-</span><span class="nx">in</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>以下操作编译失败：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">consumer</span><span class="p">(</span><span class="nx">in</span><span class="w"> </span><span class="o">&lt;-</span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">in</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w">   </span><span class="c1">// 不能发送</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nb">close</span><span class="p">(</span><span class="nx">in</span><span class="p">)</span><span class="w"> </span><span class="c1">// 不能关闭 receive-only channel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>方向限制主要用于表达所有权并防止误用。</p>
<h3 id="36-会引发-panic-的操作">3.6 会引发 panic 的操作</h3>
<p>panic 不是 channel 正常生命周期的一部分，而是错误使用导致的运行时异常。下面列出三类常见场景。</p>
<h4 id="361-向-closed-channel-发送">3.6.1 向 closed channel 发送</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">ch</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="c1">// panic: send on closed channel</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="362-重复关闭">3.6.2 重复关闭</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// panic: close of closed channel</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h4 id="363-关闭-nil-channel">3.6.3 关闭 nil channel</h4>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">ch</span><span class="w"> </span><span class="kd">chan</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nb">close</span><span class="p">(</span><span class="nx">ch</span><span class="p">)</span><span class="w"> </span><span class="c1">// panic: close of nil channel</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>这些是普通 runtime panic，可以通过 <code>recover</code> 捕获，但不应依赖 <code>recover</code> 管理 channel 生命周期。</p>
<p>与之相对，程序整体死锁会产生如下 fatal error：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">fatal error: all goroutines are asleep - deadlock!
</span></span></code></pre></td></tr></table>
</div>
</div><p>它属于 runtime fatal error，不能用常规 <code>recover</code> 恢复。</p>
<h2 id="4-同步保证">4. 同步保证</h2>
<p>channel 不只是数据队列，也建立 goroutine 之间的 happens-before 关系。</p>
<p>例如：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">done</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kd">struct</span><span class="p">{})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">value</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">&lt;-</span><span class="nx">done</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="c1">// 能观察到 value = 42</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>关闭 <code>done</code> 发生在接收方观察到关闭之前，因此前面的写入对接收方可见。</p>
<p>因此 channel 常用于任务完成通知。多个 goroutine 同时访问其他共享变量时，仍需要确保所有访问都被 channel、锁或其他同步机制正确排序。</p>
<h2 id="5-参考">5. 参考</h2>
<ul>
<li><a href="https://go.dev/ref/spec">The Go Programming Language Specification</a> — channel 类型、send/receive/close 的语义定义。</li>
<li><a href="https://go.dev/ref/mem">The Go Memory Model</a> — channel 建立的 happens-before 关系。</li>
<li><a href="https://go.dev/doc/effective_go#channels">Effective Go — Channels</a> — 官方推荐的 channel 使用模式。</li>
</ul>
<h2 id="6-延伸阅读">6. 延伸阅读</h2>
<ul>
<li><a href="https://go.dev/blog/codelab-share">Go Blog: Share Memory By Communicating</a> — channel 作为同步与通信工具的设计思路。</li>
<li><a href="https://go.dev/blog/pipelines">Go Blog: Go Concurrency Patterns: Pipelines and cancellation</a> — 多发送方/接收方、关闭责任等工程实践。</li>
<li><a href="https://go.dev/blog/advanced-go-concurrency-patterns">Go Blog: Advanced Go Concurrency Patterns</a> — <code>select</code>、nil channel 禁用 case 等模式。</li>
<li><a href="https://pkg.go.dev/golang.org/x/sync/errgroup"><code>golang.org/x/sync/errgroup</code></a> — 带错误收集和取消的 goroutine 组。</li>
<li><a href="https://github.com/sourcegraph/conc"><code>github.com/sourcegraph/conc</code></a> — 提供 <code>WaitGroup</code>、<code>Pool</code> 等结构化并发工具（Go 1.20+）。</li>
</ul>
]]></description>
    </item>
    <item>
      <title>多实例启动的 bootstrap 并发问题：从悲观锁到 upsert</title>
      <link>https://chaneyzorn.github.io/codes/bootstrap-seeding-concurrency/</link>
      <pubDate>Sat, 25 Jul 2026 14:28:48 +0800</pubDate><author>chaneyzorn#gmail#com (ChaneyZorn)</author>
      <guid>https://chaneyzorn.github.io/codes/bootstrap-seeding-concurrency/</guid>
      <description><![CDATA[<p>最近正在给公司重构一套独立的用户管理和认证鉴权组件。AuthX 启动时会做幂等初始化（bootstrap）：表结构迁移、创建默认管理员、创建预配置的服务账号。单实例下这套逻辑没有问题，但生产是多副本形态——K8s 滚动发布或 HPA 扩容时，多个副本可能同时执行这段代码。本文记录这个并发问题的处理过程：悲观锁、乐观锁、upsert 三种方案的取舍，以及 MySQL 唯一索引中 NULL 值带来的一个问题。</p>
<h2 id="并发问题">并发问题</h2>
<p>初始化的典型写法是 check-then-create：先查“管理员是否存在”，不存在则插入。AI 实现的初版方案里，存在多实例并行启动时的并发问题，两个副本的执行时序可以交错成这样：</p>

<pre class="mermaid">sequenceDiagram
    participant A as 副本 A
    participant D as 数据库
    participant B as 副本 B
    A->>D: SELECT ... WHERE username='admin' → 不存在
    B->>D: SELECT ... WHERE username='admin' → 不存在
    A->>D: INSERT users ('admin') → 成功
    B->>D: INSERT users ('admin') → ✗ 唯一键冲突</pre>
<p>后到的副本 bootstrap 报错，实例崩溃重启。重启后看到管理员已存在就会跳过，理论上可以自愈，但发布期间会伴随崩溃循环和错误日志。</p>
<p>我指出这个问题后，AI 先用 advisory lock（悲观锁）实现了基线版本，把正确性解决了。随后我提出对锁的两点顾虑——绑定 MySQL 方言、以及锁超时带来的启动失败模型——并要求评估乐观锁和 upsert 两种不依赖锁的方案。这两种方案各有前提：乐观锁要求被修改的行已经存在并带有版本字段，upsert 要求目标表有真正生效的唯一索引。</p>
<p>验证悲观锁基线时的双副本冒烟带来了一个计划外的发现：终态检查时，<code>user_auth_methods</code> 里存在<strong>两条</strong> local 密码方法，密码还不一样。用户表正常、方法表却重复，原因见本文第四节。</p>
<h2 id="乐观锁不适用于插入竞态">乐观锁：不适用于插入竞态</h2>
<p>乐观锁（optimistic locking）假设冲突很少发生：不阻止任何人操作，而是在提交修改时检查“我读取的数据在我动手期间有没有被别人改过”。经典实现是给表加一列版本号：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 读取时记下版本
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">role_id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">version</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">roles</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">role_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- version = 7
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- 更新时把版本作为条件：只有我看到的那个版本还在，更新才生效
</span></span></span><span class="line"><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">roles</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;admin-v2&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">version</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">role_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">7</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- RowsAffected = 1：成功；= 0：期间有人改过，重读重试
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>它叫“乐观”，是因为全程不加锁，冲突以“更新失败后重试”的方式事后发现。适合读多写少、冲突可重试的场景（比如未来的角色权限编辑）。</p>
<p>但它解决的是<strong>并发更新同一行</strong>。初始化竞态是<strong>并发插入不存在的行</strong>——行还不存在，没有版本号可比较，CAS 无从谈起。这也是我在评审中排除它的原因：对数据模型的前提（行已存在、有版本列）在这个场景不成立。</p>
<h2 id="悲观锁advisory-lock有效但引入依赖">悲观锁（advisory lock）：有效，但引入依赖</h2>
<p>悲观锁（pessimistic locking）相反：假设冲突一定会发生，先把资源锁住，让别人排队。数据库里的行锁（<code>SELECT ... FOR UPDATE</code>）、表锁都属此类。正确性直接，代价是引入了等待。</p>
<p>我们要锁的不是某行数据（行还不存在），而是“初始化”这个动作本身。这正好对应 MySQL 的<strong>命名咨询锁（named advisory lock）</strong>：一把与任何表无关、按名字获取的全局锁，语义是“同名锁同一时刻只有一个连接能持有”。基本用法：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">GET_LOCK</span><span class="p">(</span><span class="s1">&#39;authx_bootstrap_seed&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 1 = 拿到；0 = 10 秒超时；NULL = 出错
</span></span></span><span class="line"><span class="cl"><span class="c1">-- ... 临界区：初始化 ...
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">RELEASE_LOCK</span><span class="p">(</span><span class="s1">&#39;authx_bootstrap_seed&#39;</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 释放（连接断开也会自动释放）
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>实际使用中有三个细节需要注意：</p>
<ol>
<li>
<p><strong>加锁和解锁必须在同一条连接上</strong>。锁的持有者是连接；用连接池随手执行两条语句，可能落在两条连接上，导致锁没有生效、或锁无法被释放。我们的实现是显式持有一条专用连接，直到临界区结束：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">sqlDB</span><span class="p">.</span><span class="nf">Conn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span><span class="w"> </span><span class="c1">// 专用连接，不是池里随便拿一条</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s">&#34;SELECT GET_LOCK(?, ?)&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">timeoutSec</span><span class="p">).</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">locked</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">err</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">defer</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">conn</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;SELECT RELEASE_LOCK(?)&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">conn</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}()</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>返回值三种都要处理</strong>：<code>1</code> = 拿到，<code>0</code> = 超时，<code>NULL</code> = 出错（如锁名超过 64 字符）。</p>
</li>
<li>
<p><strong>解锁路径必须覆盖所有退出分支</strong>（包括 panic）——<code>defer</code> 是唯一可靠的方式。</p>
</li>
</ol>
<p>golang-migrate 也使用同样的机制保证多实例迁移不冲突（它的 MySQL 驱动里同样是 <code>SELECT GET_LOCK(?, 10)</code>，见参考链接）。基线实现后，双副本冒烟结果：0 失败、恰好 1 个管理员、1 条密码方法，正确性得到验证。</p>
<p>但这个方案有两个无法回避的代价：</p>
<ol>
<li><strong>绑定底层数据库选型</strong>。<code>GET_LOCK</code> 是手写的 MySQL 方言，没有任何抽象层。PostgreSQL 虽有 <code>pg_advisory_lock</code>，但名称、语义、超时行为都不同，日后要兼容就是债务。</li>
<li><strong>悲观锁的失败模型</strong>。锁意味着等待，等待意味着超时，超时意味着副本启动失败并进入崩溃循环——只是从“撞唯一键崩溃”换成了“等锁超时崩溃”。概率不高，但这个故障模式是我们主动引入的。</li>
</ol>
<p>DDIA 第 9 章《分布式系统的麻烦》对这类失效模式有更系统的讨论：网络延迟和进程暂停都可能导致持锁实例无法及时续租，锁超时后被其他实例获得，进而产生并发写入（<a href="https://ddia.vonng.com/ch9/">参考：DDIA 第 9 章</a>）。</p>
<h2 id="upsert由唯一索引保证原子性">upsert：由唯一索引保证原子性</h2>
<p>唯一索引本身就是数据库内置的串行点，违反约束的插入会被数据库原子地拒绝。upsert 把“冲突了怎么办”变成插入语句自身语义的一部分：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- MySQL：GORM MySQL driver 生成主键自赋值，冲突行保持不动
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;admin&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ON</span><span class="w"> </span><span class="n">DUPLICATE</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">user_id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;admin&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ON</span><span class="w"> </span><span class="n">CONFLICT</span><span class="w"> </span><span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">)</span><span class="w"> </span><span class="k">DO</span><span class="w"> </span><span class="k">NOTHING</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>整个操作在数据库内部是原子的，不存在“查到插入之间”的窗口——竞态从语义上被消除，不需要任何锁。</p>
<p>工程上不直接写方言 SQL：GORM 的 <code>clause.OnConflict</code> 会按驱动生成对应方言（MySQL → <code>ON DUPLICATE KEY UPDATE</code>，PostgreSQL → <code>ON CONFLICT DO NOTHING</code>），应用代码保持方言中立：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">tx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">db</span><span class="p">.</span><span class="nf">Clauses</span><span class="p">(</span><span class="nx">clause</span><span class="p">.</span><span class="nx">OnConflict</span><span class="p">{</span><span class="nx">DoNothing</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">}).</span><span class="nf">Create</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// 在当前 MySQL DSN 配置下，RowsAffected == 1 表示插入，</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// RowsAffected == 0 表示冲突行保持不变。</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>单行管理员初始化使用 <code>RowsAffected</code> 判断本实例是否插入了初始密码，以决定是否输出一次性初始凭证。服务账号使用批量 upsert，不依据 <code>RowsAffected</code> 判断具体新增项：批量语句只返回汇总值，并发执行时无法据此确定每个名称由哪个实例插入。</p>
<p>前提只有一个：<strong>唯一索引必须真的生效</strong>。用户表没问题（<code>(tenant_id, username)</code> 一直在工作），问题出在方法表。</p>
<h2 id="mysql-唯一索引中的-null">MySQL 唯一索引中的 NULL</h2>
<p><code>user_auth_methods</code> 的唯一索引是 <code>(method_type, identity_provider_id, external_user_id)</code>。原方案为没有外部身份源的 local 密码方法写入 NULL。用两行 SQL 就能看到后果：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">user_auth_methods</span><span class="w"> </span><span class="p">(</span><span class="n">method_type</span><span class="p">,</span><span class="w"> </span><span class="n">identity_provider_id</span><span class="p">,</span><span class="w"> </span><span class="n">external_user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;local&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 成功
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">user_auth_methods</span><span class="w"> </span><span class="p">(</span><span class="n">method_type</span><span class="p">,</span><span class="w"> </span><span class="n">identity_provider_id</span><span class="p">,</span><span class="w"> </span><span class="n">external_user_id</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;local&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">);</span><span class="w">  </span><span class="c1">-- 依然成功！唯一索引没有拦截
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>原因：<strong>MySQL（遵循 SQL 标准）认为 NULL 不等于 NULL</strong>——唯一索引中任何一列为 NULL，整行都不参与判重，索引因此失效。这就是双副本冒烟中两条 local 方法并存的原因（MySQL 官方文档对这一行为的说明见参考链接）。</p>
<p>修复索引有几个候选：</p>
<ul>
<li><strong><code>UNIQUE(user_id, method_type)</code>，不可行。</strong> 它假设一个用户每种方法只有一个，但 passkey 是典型的多设备多凭证（手机一个、笔记本一个），这个索引会误伤未来的合法业务。</li>
<li><strong>生成列做部分唯一索引。</strong> MySQL 支持（<code>IF(method_type='local', user_id, NULL)</code> 加唯一索引，利用“NULL 不判重”反向构造，只有 local 行参与判重），方案精确，但引入方言化的 schema 复杂度——和避开方言的目标相矛盾。</li>
<li><strong>写 0 而不是 NULL（最终选择）</strong>。无身份源的方法把 <code>identity_provider_id</code> 存为 <code>0</code>，列定义同步调整为 <code>NOT NULL DEFAULT 0</code>，索引对所有方法类型生效。配套约定 <code>external_user_id</code> 的取值：<code>local</code>/<code>totp</code> 写 <code>user_id</code> 字符串（天然全局唯一，避开跨租户同名用户的碰撞），<code>passkey</code> 写 credential_id。</li>
</ul>
<h2 id="批量初始化服务账号">批量初始化服务账号</h2>
<p>服务账号列表先排序、去重，再通过多行 upsert 分批写入。排序使并发副本按相同顺序获取唯一索引锁；分批限制单条 SQL 的大小，GORM 在当前配置下用事务包裹多个批次。</p>
<p>同一批可以同时包含已有账号和新增账号：已有名称命中 <code>(tenant_id, username)</code> 唯一索引后保持不变，新增名称被插入。新旧版本副本并行启动时，数据库终态是原有集合与各版本配置集合的并集。启动配置只负责增量预创建，不负责更新、禁用、重命名或删除已有账号。</p>
<p>写入完成后按租户和名称批量回读，确认所有配置名称都存在且 <code>type=service_account</code>。若名称已被自然人用户占用，bootstrap 返回配置冲突，不修改已有用户。</p>
<h2 id="最终方案与验证">最终方案与验证</h2>
<ul>
<li>
<p>store 层为管理员和认证方法提供单行 IfAbsent 方法，用 <code>RowsAffected</code> 判断是否由本实例插入：</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">s</span><span class="w"> </span><span class="o">*</span><span class="nx">Store</span><span class="p">)</span><span class="w"> </span><span class="nf">CreateUserIfAbsent</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="o">*</span><span class="nx">User</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="kt">bool</span><span class="p">,</span><span class="w"> </span><span class="kt">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">tx</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">s</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">).</span><span class="nf">Clauses</span><span class="p">(</span><span class="nx">clause</span><span class="p">.</span><span class="nx">OnConflict</span><span class="p">{</span><span class="nx">DoNothing</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">}).</span><span class="nf">Create</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">Error</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;failed to create user if absent: %w&#34;</span><span class="p">,</span><span class="w"> </span><span class="nf">translateErr</span><span class="p">(</span><span class="nx">tx</span><span class="p">.</span><span class="nx">Error</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">RowsAffected</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>service account 使用批量 IfAbsent 方法，不返回逐行创建结果；bootstrap 在批量回读验证后记录聚合的 ensured 日志；</p>
</li>
<li>
<p>bootstrap 初始化使用 upsert：管理员插入后按唯一键回读，无论输赢都拿到最终状态；local 方法按 <code>identity_provider_id=0</code>、<code>external_user_id=user_id</code> 字符串写入；</p>
</li>
<li>
<p>验证覆盖单行 IfAbsent、服务账号批量新增与重复执行、新旧列表增量合并、名称类型冲突，以及双副本启动终态。</p>
</li>
</ul>
<h2 id="什么时候仍然需要锁">什么时候仍然需要锁</h2>
<p>锁并非在所有场景都不适用，只是在这个场景被更合适的工具取代了。如果未来出现这些初始化任务，锁（或事务）会回来：</p>
<ul>
<li>跨多行、多表的原子初始化（无法归结为单行唯一约束）；</li>
<li>无法建立唯一索引的场景（如按时间窗、按条件去重）；</li>
<li>需要“全局只许一个实例执行”的运维动作（如全库数据回填）。</li>
</ul>
<p>其中“全局只许一个实例执行”在分布式系统中本质上是共识问题，DDIA 第 10 章《一致性与共识》讨论了在故障存在时如何通过共识算法实现这一点（<a href="https://ddia.vonng.com/ch10/">参考：DDIA 第 10 章</a>）。</p>
<p>原则是：<strong>能用唯一约束把正确性交给数据库的，就不要自己拿锁</strong>。悲观锁可以快速建立正确性基线，但基线成立之后，仍值得回头找一个不依赖它的方案。</p>
<h2 id="参考链接">参考链接</h2>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/locking-functions.html">MySQL Locking Functions: GET_LOCK() — MySQL Documentation</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/create-index.html">MySQL CREATE INDEX: UNIQUE 索引与 NULL — MySQL Documentation</a></li>
<li><a href="https://github.com/golang-migrate/migrate">golang-migrate/migrate — GitHub</a></li>
<li><a href="https://gorm.io/docs/create.html">GORM: Upsert / On Conflict — GORM Guides</a></li>
</ul>
<h2 id="推荐阅读">推荐阅读</h2>
<ul>
<li><a href="https://ddia.vonng.com/ch9/">DDIA（第二版）第 9 章：分布式系统的麻烦</a>——系统梳理网络延迟、时钟偏移、进程暂停等部分失效问题，以及分布式锁与租约的失效模式（含隔离令牌 fencing token），是本文悲观锁失败模型一节的理论背景。</li>
<li><a href="https://ddia.vonng.com/ch10/">DDIA（第二版）第 10 章：一致性与共识</a>——讨论线性一致性与共识算法，即在故障存在时如何让分布式系统对外表现得像单一可靠系统，是「全局只许一个实例执行」这一类需求的理论基础。</li>
</ul>
]]></description>
    </item>
  </channel>
</rss>
