<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>thinkeridea博客</title>
  <icon>https://www.gravatar.com/avatar/9d6f60998913bb76135b4779bbb90154</icon>
  <subtitle>thinkeridea</subtitle>
  <link href="https://blog.thinkeridea.com/atom.xml" rel="self"/>
  
  <link href="https://blog.thinkeridea.com/"/>
  <updated>2021-01-02T12:11:28.676Z</updated>
  <id>https://blog.thinkeridea.com/</id>
  
  <author>
    <name>戚银</name>
    <email>qiyin@thinkeridea.com</email>
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>【Go】四舍五入在go语言中为何如此困难</title>
    <link href="https://blog.thinkeridea.com/202101/go/round.html"/>
    <id>https://blog.thinkeridea.com/202101/go/round.html</id>
    <published>2021-01-02T03:57:22.000Z</published>
    <updated>2021-01-02T12:11:28.676Z</updated>
    
    <content type="html"><![CDATA[<p>四舍五入是一个非常常见的功能，在流行语言标准库中往往存在 <code>Round</code> 的功能，它最少支持常用的 <code>Round half up</code> 算法。</p><p>而在 Go 语言中这似乎成为了难题，在 stackoverflow 上搜索 <code>[go] Round</code> 会存在大量相关提问，Go 1.10 开始才出现 <code>math.Round</code> 的身影，本以为 <code>Round</code> 的疑问就此结束，但是一看函数注释 <code>Round returns the nearest integer, rounding half away from zero</code> ，这是并不常用的 <code>Round half away from zero</code> 实现呀，说白了就是我们理解的 <code>Round</code> 阉割版，精度为 0 的 <code>Round half up</code> 实现，<code>Round half away from zero</code> 的存在是为了提供一种高效的通过二进制方法得结果，可以作为 <code>Round</code> 精度为 0 时的高效实现分支。</p><p>带着对 <code>Round</code> 的‘敬畏’，我在 stackoverflow 翻阅大量关于 <code>Round</code> 问题，开启寻求最佳的答案，本文整理我认为有用的实现，简单分析它们的优缺点，对于不想逐步了解，想直接看结果的小伙伴，可以直接看文末的最佳实现，或者跳转 <a href="https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go#L12">exmath.Round</a> 直接看源码和使用吧！</p><a id="more"></a><h2 id="Round-第一弹"><a href="#Round-第一弹" class="headerlink" title="Round 第一弹"></a>Round 第一弹</h2><p>在 <a href="https://stackoverflow.com/questions/39544571/golang-round-to-nearest-0-05">stackoverflow</a> 问题中的最佳答案首先获得我的关注，它在 <a href="https://github.com/icza/gox/blob/master/mathx/mathx.go#L26">mathx.Round</a> 被开源，以下是代码实现：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//source: https://github.com/icza/gox/blob/master/mathx/mathx.go</span></span><br><span class="line"><span class="keyword">package</span> mathx</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;math&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Round returns x rounded to the given unit.</span></span><br><span class="line"><span class="comment">// Tip: x is &quot;arbitrary&quot;, maybe greater than 1.</span></span><br><span class="line"><span class="comment">// For example:</span></span><br><span class="line"><span class="comment">//     Round(0.363636, 0.001) // 0.364</span></span><br><span class="line"><span class="comment">//     Round(0.363636, 0.01)  // 0.36</span></span><br><span class="line"><span class="comment">//     Round(0.363636, 0.1)   // 0.4</span></span><br><span class="line"><span class="comment">//     Round(0.363636, 0.05)  // 0.35</span></span><br><span class="line"><span class="comment">//     Round(3.2, 1)          // 3</span></span><br><span class="line"><span class="comment">//     Round(32, 5)           // 30</span></span><br><span class="line"><span class="comment">//     Round(33, 5)           // 35</span></span><br><span class="line"><span class="comment">//     Round(32, 10)          // 30</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// For details, see https://stackoverflow.com/a/39544897/1705598</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(x, unit <span class="keyword">float64</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> math.Round(x/unit) * unit</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个实现非常的简洁，借用了 <code>math.Round</code>，由此看来 <code>math.Round</code> 还是很有价值的，大致测试了它的性能一次运算大概 <code>0.4ns</code>，这非常的快。</p><p>但是我也很快发现了它的问题，就是精度问题，这个是问题中一个回答的解释让我有了警觉，并开始了实验。他认为使用浮点数确定精度（<code>mathx.Round</code>的第二个参数）是不恰当的，因为浮点数本身并不精确，例如 0.05 在64位IEEE浮点数中，可能会将其存储为<code>0.05000000000000000277555756156289135105907917022705078125</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//source: https://play.golang.org/p/0uN1kEG30kI</span></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;math&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">f := <span class="number">12.15807659924030304</span></span><br><span class="line">fmt.Println(Round(f, <span class="number">0.0001</span>)) <span class="comment">// 12.158100000000001</span></span><br><span class="line"></span><br><span class="line">f = <span class="number">0.15807659924030304</span></span><br><span class="line">fmt.Println(Round(f, <span class="number">0.0001</span>)) <span class="comment">// 0.15810000000000002</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(x, unit <span class="keyword">float64</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> math.Round(x/unit) * unit</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码可以在 <a href="https://play.golang.org/p/0uN1kEG30kI">Go Playground</a> 上运行，得到结果并非如期望那般，这个问题主要出现在 <code>math.Round(x/unit)</code> 与 <code>unit</code> 运算时，<code>math.Round</code> 运算后一定会是一个精确的整数，但是 <code>0.0001</code> 的精度存在误差，所以导致最终得到的结果精度出现了偏差。</p><h2 id="格式化与反解析"><a href="#格式化与反解析" class="headerlink" title="格式化与反解析"></a>格式化与反解析</h2><p>在这个问题中也有人提出了先用 <code>fmt.Sprintf</code> 对结果进行格式化，然后再采用 <code>strconv.ParseFloat</code> 反向解析，<a href="https://play.golang.org/p/jxILFBYBEF">Go Playground</a> 代码在这个里。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">source: https:<span class="comment">//play.golang.org/p/jxILFBYBEF</span></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;strconv&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Println(Round(<span class="number">0.363636</span>, <span class="number">0.05</span>)) <span class="comment">// 0.35</span></span><br><span class="line">fmt.Println(Round(<span class="number">3.232</span>, <span class="number">0.05</span>))    <span class="comment">// 3.25</span></span><br><span class="line">fmt.Println(Round(<span class="number">0.4888</span>, <span class="number">0.05</span>))   <span class="comment">// 0.5</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(x, unit <span class="keyword">float64</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> rounded <span class="keyword">float64</span></span><br><span class="line"><span class="keyword">if</span> x &gt; <span class="number">0</span> &#123;</span><br><span class="line">rounded = <span class="keyword">float64</span>(<span class="keyword">int64</span>(x/unit+<span class="number">0.5</span>)) * unit</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">rounded = <span class="keyword">float64</span>(<span class="keyword">int64</span>(x/unit<span class="number">-0.5</span>)) * unit</span><br><span class="line">&#125;</span><br><span class="line">formatted, err := strconv.ParseFloat(fmt.Sprintf(<span class="string">&quot;%.2f&quot;</span>, rounded), <span class="number">64</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> rounded</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> formatted</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码中有点问题，第一是结果不对，和我们理解的存在差异，后来一看第二个参数传错了，应该是 <code>0.01</code>，我想试着调整调整精度吧，我改成了 <code>0.0001</code> 之后发现一直都是保持小数点后两位，我细细研究了下这段代码的逻辑，发现 <code>fmt.Sprintf(&quot;%.2f&quot;, rounded)</code> 中写死了保留的位数，所以它并不通用，我尝试如下简单调整一下使其生效。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;strconv&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">f := <span class="number">12.15807659924030304</span></span><br><span class="line">fmt.Println(Round(f, <span class="number">0.0001</span>)) <span class="comment">// 12.1581</span></span><br><span class="line"></span><br><span class="line">f = <span class="number">0.15807659924030304</span></span><br><span class="line">fmt.Println(Round(f, <span class="number">0.0001</span>)) <span class="comment">// 0.1581</span></span><br><span class="line"></span><br><span class="line">fmt.Println(Round(<span class="number">0.363636</span>, <span class="number">0.0001</span>)) <span class="comment">// 0.3636</span></span><br><span class="line">fmt.Println(Round(<span class="number">3.232</span>, <span class="number">0.0001</span>))    <span class="comment">// 3.232</span></span><br><span class="line">fmt.Println(Round(<span class="number">0.4888</span>, <span class="number">0.0001</span>))   <span class="comment">// 0.4888</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(x, unit <span class="keyword">float64</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> rounded <span class="keyword">float64</span></span><br><span class="line"><span class="keyword">if</span> x &gt; <span class="number">0</span> &#123;</span><br><span class="line">rounded = <span class="keyword">float64</span>(<span class="keyword">int64</span>(x/unit+<span class="number">0.5</span>)) * unit</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">rounded = <span class="keyword">float64</span>(<span class="keyword">int64</span>(x/unit<span class="number">-0.5</span>)) * unit</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> precision <span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> unit &lt; <span class="number">1</span> &#123;</span><br><span class="line">precision++</span><br><span class="line">unit *= <span class="number">10</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">formatted, err := strconv.ParseFloat(fmt.Sprintf(<span class="string">&quot;%.&quot;</span>+strconv.Itoa(precision)+<span class="string">&quot;f&quot;</span>, rounded), <span class="number">64</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> rounded</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> formatted</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>确实获得了满意的精准度，但是其性能也非常客观，达到了 <code>215ns/op</code>，暂时看来如果追求精度，这个算法目前是比较完美的。</p><h2 id="大道至简"><a href="#大道至简" class="headerlink" title="大道至简"></a>大道至简</h2><p>很快我发现了另一个极简的算法，它的精度和速度都非常的高，实现还特别精简：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exmath&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    f := <span class="number">0.15807659924030304</span></span><br><span class="line">    fmt.Println(<span class="keyword">float64</span>(<span class="keyword">int64</span>(f*<span class="number">10000</span>+<span class="number">0.5</span>)) / <span class="number">10000</span>) <span class="comment">// 0.1581</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这并不通用，除非像以下这么包装：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(x, unit <span class="keyword">float64</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">float64</span>(<span class="keyword">int64</span>(x*unit+<span class="number">0.5</span>)) / unit</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>unit</code> 参数和之前的概念不同了，保留一位小数 <code>uint =10</code>，只是整数 <code>uint=1</code>, 想对整数部分进行精度控制 <code>uint=0.01</code> 例如： <code>Round(1555.15807659924030304, 0.01) = 1600</code>，<code>Round(1555.15807659924030304, 1) = 1555</code>，<code>Round(1555.15807659924030304, 10000) = 1555.1581</code>。</p><p>这似乎就是终极答案了吧，等等……</p><h2 id="终极方案"><a href="#终极方案" class="headerlink" title="终极方案"></a>终极方案</h2><p>上面的方法够简单，也够高效，但是 api 不太友好，第二个参数不够直观，带了一定的心智负担，其它语言都是传递保留多少位小数，例如 <code>Round(1555.15807659924030304, 0) = 1555</code>，<code>Round(1555.15807659924030304, 2) = 1555.16</code>，<code>Round(1555.15807659924030304, -2) = 1600</code>，这样的交互才符合人性啊。</p><p>别急我在 <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 开源了 <a href="https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go">exmath.Round</a>，其算法符合通用语言 <code>Round</code> 实现，且遵循 <code>Round half up</code> 算法要求，其性能方面在 <code>3.50ns/op</code>, 具体可以参看<a href="https://github.com/thinkeridea/go-extend/pull/13">调优exmath.Round算法</a>， 具体代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//source: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> exmath</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;math&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Round 四舍五入，ROUND_HALF_UP 模式实现</span></span><br><span class="line"><span class="comment">// 返回将 val 根据指定精度 precision（十进制小数点后数字的数目）进行四舍五入的结果。precision 也可以是负数或零。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Round</span><span class="params">(val <span class="keyword">float64</span>, precision <span class="keyword">int</span>)</span> <span class="title">float64</span></span> &#123;</span><br><span class="line">p := math.Pow10(precision)</span><br><span class="line"><span class="keyword">return</span> math.Floor(val*p+<span class="number">0.5</span>) / p</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>Round</code> 功能虽简单，但是受到 <code>float</code> 精度影响，仍然有很多人在四处寻找稳定高效的算法，参阅了大多数资料后精简出 <a href="https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go">exmath.Round</a> 方法，期望对其他开发者有所帮助，至于其精度使用了大量的测试用例，没有超过 <code>float</code> 精度范围时并没有出现精度问题，未知问题等待社区检验，具体测试用例参见 <a href="https://github.com/thinkeridea/go-extend/blob/main/exmath/round_test.go">round_test</a>。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;四舍五入是一个非常常见的功能，在流行语言标准库中往往存在 &lt;code&gt;Round&lt;/code&gt; 的功能，它最少支持常用的 &lt;code&gt;Round half up&lt;/code&gt; 算法。&lt;/p&gt;
&lt;p&gt;而在 Go 语言中这似乎成为了难题，在 stackoverflow 上搜索 &lt;code&gt;[go] Round&lt;/code&gt; 会存在大量相关提问，Go 1.10 开始才出现 &lt;code&gt;math.Round&lt;/code&gt; 的身影，本以为 &lt;code&gt;Round&lt;/code&gt; 的疑问就此结束，但是一看函数注释 &lt;code&gt;Round returns the nearest integer, rounding half away from zero&lt;/code&gt; ，这是并不常用的 &lt;code&gt;Round half away from zero&lt;/code&gt; 实现呀，说白了就是我们理解的 &lt;code&gt;Round&lt;/code&gt; 阉割版，精度为 0 的 &lt;code&gt;Round half up&lt;/code&gt; 实现，&lt;code&gt;Round half away from zero&lt;/code&gt; 的存在是为了提供一种高效的通过二进制方法得结果，可以作为 &lt;code&gt;Round&lt;/code&gt; 精度为 0 时的高效实现分支。&lt;/p&gt;
&lt;p&gt;带着对 &lt;code&gt;Round&lt;/code&gt; 的‘敬畏’，我在 stackoverflow 翻阅大量关于 &lt;code&gt;Round&lt;/code&gt; 问题，开启寻求最佳的答案，本文整理我认为有用的实现，简单分析它们的优缺点，对于不想逐步了解，想直接看结果的小伙伴，可以直接看文末的最佳实现，或者跳转 &lt;a href=&quot;https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go#L12&quot;&gt;exmath.Round&lt;/a&gt; 直接看源码和使用吧！&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="round" scheme="https://blog.thinkeridea.com/tags/round/"/>
    
    <category term="half up" scheme="https://blog.thinkeridea.com/tags/half-up/"/>
    
    <category term="exmath" scheme="https://blog.thinkeridea.com/tags/exmath/"/>
    
    <category term="math" scheme="https://blog.thinkeridea.com/tags/math/"/>
    
  </entry>
  
  <entry>
    <title>【Go】我与sync.Once的爱恨纠缠</title>
    <link href="https://blog.thinkeridea.com/202101/go/exsync/once.html"/>
    <id>https://blog.thinkeridea.com/202101/go/exsync/once.html</id>
    <published>2021-01-01T07:26:31.000Z</published>
    <updated>2021-01-01T13:00:11.752Z</updated>
    
    <content type="html"><![CDATA[<p>官方描述 <code>Once is an object that will perform exactly one action</code>, 即 <code>Once</code> 是一个对象,它提供了保证某个动作只被执行一次功能，最典型的场景就是单例模式，<code>Once</code> 可用于任何符合 “exactly once” 语义的场景。</p><h2 id="sync-Once-的用法"><a href="#sync-Once-的用法" class="headerlink" title="sync.Once 的用法"></a>sync.Once 的用法</h2><p>在多数情况下，<code>sync.Once</code> 被用于控制变量的初始化，这个变量的读写通常遵循单例模式，满足这三个条件：</p><ul><li>当且仅当第一次读某个变量时，进行初始化（写操作）</li><li>变量被初始化过程中，所有读都被阻塞（读操作；当变量初始化完成后，读操作继续进行）</li><li>变量仅初始化一次，初始化完成后驻留在内存里</li></ul><a id="more"></a><p>在标准库中不乏有大量 <code>sync.Once</code> 的使用案例，在 <code>strings</code> 包中 <code>replace.go</code> 里实现字符串批量替换功能时，需要预编译生成替换规则，即采用不同的替换算法并创建相关算法实例，因 <code>strings.Replacer</code> 实现是线程安全且支持规则复用，在第一次解析替换规则并创建对应算法实例后，可以并发的进行字符串替换操作，避免多次解析替换规则浪费资源。</p><p>先看一下 <code>strings.Replacer</code> 的结构定义：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: strings/replace.go</span></span><br><span class="line"><span class="keyword">type</span> Replacer <span class="keyword">struct</span> &#123;</span><br><span class="line">once   sync.Once <span class="comment">// guards buildOnce method</span></span><br><span class="line">r      replacer</span><br><span class="line">oldnew []<span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里定义了 <code>once   sync.Once</code> 用来控制 <code>r  replacer</code> 替换算法初始化，当我们使用 <code>strings.NewReplacer</code> 创建 <code>strings.Replacer</code> 时，这里采用惰性算法，并没有在这时进行 <code>build</code> 解析替换规则并创建对应算法实例，而是在执行替换时( <code>Replacer.Replace</code> 和 <code>Replacer.WriteString</code>)进行的, <code>r.once.Do(r.buildOnce)</code> 使用 <code>sync.Once</code> 的 <code>Do</code> 方法保证只有在首次执行时才会执行 <code>buildOnce</code> 方法，而在 <code>buildOnce</code> 中调用 <code>build</code> 解析替换规则并创建对应算法实例，在 <code>buildOnce</code> 中进行赋值。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: strings/replace.go</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewReplacer</span><span class="params">(oldnew ...<span class="keyword">string</span>)</span> *<span class="title">Replacer</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(oldnew)%<span class="number">2</span> == <span class="number">1</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;strings.NewReplacer: odd argument count&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> &amp;Replacer&#123;oldnew: <span class="built_in">append</span>([]<span class="keyword">string</span>(<span class="literal">nil</span>), oldnew...)&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *Replacer)</span> <span class="title">buildOnce</span><span class="params">()</span></span> &#123;</span><br><span class="line">r.r = r.build()</span><br><span class="line">r.oldnew = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *Replacer)</span> <span class="title">build</span><span class="params">()</span> <span class="title">replacer</span></span> &#123;</span><br><span class="line">    ....</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *Replacer)</span> <span class="title">Replace</span><span class="params">(s <span class="keyword">string</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">r.once.Do(r.buildOnce)</span><br><span class="line"><span class="keyword">return</span> r.r.Replace(s)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *Replacer)</span> <span class="title">WriteString</span><span class="params">(w io.Writer, s <span class="keyword">string</span>)</span> <span class="params">(n <span class="keyword">int</span>, err error)</span></span> &#123;</span><br><span class="line">r.once.Do(r.buildOnce)</span><br><span class="line"><span class="keyword">return</span> r.r.WriteString(w, s)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>简单来说，<code>once.Do</code> 中的函数只会执行一次，并保证 <code>once.Do</code> 返回时，传入 <code>Do</code> 的函数已经执行完成。多个 <code>goroutine</code> 同时执行 <code>once.Do</code> 的时候，可以保证抢占到 <code>once.Do</code> 执行权的 <code>goroutine</code> 执行完 <code>once.Do</code> 后，其他 <code>goroutine</code> 才能得到返回。 </p><p><code>once.Do</code> 接收一个函数作为参数，该函数不接受任何参数，不返回任何参数。具体做什么由使用方决定，错误处理也由使用方控制，对函数初始化的结果也由使用方进行保存。</p><p>这给出了一种错误处理的例子 <code>exec.closeOnce</code>，<code>exec.closeOnce</code> 保证了重复关闭文件，永远只执行一次，并且总是返回首次关闭产生的错误信息：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: os/exec/exec.go</span></span><br><span class="line"><span class="keyword">type</span> closeOnce <span class="keyword">struct</span> &#123;</span><br><span class="line">*os.File</span><br><span class="line"></span><br><span class="line">once sync.Once</span><br><span class="line">err  error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">Close</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">c.once.Do(c.<span class="built_in">close</span>)</span><br><span class="line"><span class="keyword">return</span> c.err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">close</span><span class="params">()</span></span> &#123;</span><br><span class="line">c.err = c.File.Close()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="对-sync-Once-的爱与恨"><a href="#对-sync-Once-的爱与恨" class="headerlink" title="对 sync.Once 的爱与恨"></a>对 sync.Once 的爱与恨</h2><p><code>Once</code> 的实现非常的灵活、简洁、高效，排除注释部分 <code>Once</code> 仅用 17 行实现，且单次执行时间在 0.3ns 左右。这让我十分敬佩，对它可谓喜爱至极，但因为它的通用性，在使用 <code>Once</code> 时给我带来了一些小小的负担，这也成了我极少的使用它的原因。</p><p><code>Once</code> 只保证调用安全性（即线程安全以及只执行一次动作函数），但是细心的朋友一定发现了我们往往需要配对定义 <code>Once</code> 和业务实例变量，极少使用的情况下（如上述两个例子）看起来并没有什么负担，但是如果我们项目中有大量实例进行管理时（一般是集中管理，便于解决依赖问题），这时就会变得有点丑陋。</p><p>一个实际的业务场景，我有一个 <code>http</code> 服务，它有数百个组件实例，我们创建了一个 <code>APP</code> 用来管理所有实例的初始化、依赖关系，从而保证各个组件依赖其接口，相互之间进行解耦，也使得每个组件的配置（初始化参数）、依赖易于管理，不过我们常常对单例实例在 <code>http</code> 服务启动时进行初始化，这样避免使用 <code>Once</code>，且可以在 <code>http</code> 服务启动时暴露外部依赖问题（数据库、其它服务等）。</p><p>这个 <code>http</code> 服务需要很多辅助命令，每个命令负责极少的工作，如果我在命令启动时使用 <code>APP</code> 初始化所有组件，这造成了大量的资源浪费。我单独实现一个 <code>Command</code> 依赖管理组件，它大量使用 <code>Once</code> 保证各个组件只在第一次使用时进行初始化，这给我带来了一些困扰，我大量定义 <code>Once</code> 的实例，且它和具体的组件实例没有关联，我在使用时需要非常的小心。</p><p>使用过 <a href="https://github.com/thinkeridea/go-extend/tree/v1.1.2/pool">go-extend/pool</a> 中的 <a href="https://github.com/thinkeridea/go-extend/blob/v1.1.2/pool/buffer.go#L33">pool.BufferPool</a> 的朋友如果留意其源码的话会发现其中定义了一些 <code>sync.Once</code> 的实例，这相对上诉场景却是相对少的，以下便是 <a href="https://github.com/thinkeridea/go-extend/blob/v1.1.2/pool/buffer.go#L33">pool.BufferPool</a> 中的部分代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: https://github.com/thinkeridea/go-extend/blob/v1.1.2/pool/buffer.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> pool</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> (</span><br><span class="line">buff64   *sync.Pool</span><br><span class="line">buff128  *sync.Pool</span><br><span class="line">buff512  *sync.Pool</span><br><span class="line">buff1024 *sync.Pool</span><br><span class="line">buff2048 *sync.Pool</span><br><span class="line">buff4096 *sync.Pool</span><br><span class="line">buff8192 *sync.Pool</span><br><span class="line"></span><br><span class="line">buff64One   sync.Once</span><br><span class="line">buff128One  sync.Once</span><br><span class="line">buff512One  sync.Once</span><br><span class="line">buff1024One sync.Once</span><br><span class="line">buff2048One sync.Once</span><br><span class="line">buff4096One sync.Once</span><br><span class="line">buff8192One sync.Once</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> pool sync.Pool</span><br><span class="line"></span><br><span class="line"><span class="comment">// BufferPool bytes.Buffer 的 sync.Pool 接口</span></span><br><span class="line"><span class="comment">// 可以直接 Get *bytes.Buffer 并 Reset Buffer</span></span><br><span class="line"><span class="keyword">type</span> BufferPool <span class="keyword">interface</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Get 从 Pool 中获取一个 *bytes.Buffer 实例, 该实例已经被 Reset</span></span><br><span class="line">Get() *bytes.Buffer</span><br><span class="line"><span class="comment">// Put 把 *bytes.Buffer 放回 Pool 中</span></span><br><span class="line">Put(*bytes.Buffer)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newBufferPool</span><span class="params">(size <span class="keyword">int</span>)</span> *<span class="title">sync</span>.<span class="title">Pool</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, size))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetBuff64 获取一个初始容量为 64 的 *bytes.Buffer Pool</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetBuff64</span><span class="params">()</span> <span class="title">BufferPool</span></span> &#123;</span><br><span class="line">buff64One.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">buff64 = newBufferPool(<span class="number">64</span>)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> (*pool)(buff64)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上诉代码中定义了 <code>buff64One</code> 到 <code>buff8192One</code> 7个 <code>Once</code> 的实例，且对应的存在 <code>buff64</code> 到 <code>buff8192</code> 的业务实例，我在 <code>GetBuff64</code> 中必须小心使用 <code>Once</code> 实例，避免错误使用导致对应的实例未被初始化，而且上诉的代码看起来还有一些丑陋。</p><h2 id="探寻缓和与-sync-Once-的尴尬"><a href="#探寻缓和与-sync-Once-的尴尬" class="headerlink" title="探寻缓和与 sync.Once 的尴尬"></a>探寻缓和与 sync.Once 的尴尬</h2><p>鉴于我对 <code>sync.Once</code> 灵活、简洁、高效的喜爱，不能仅仅因为它的“吝啬”（极简的功能）便与之诀别，促使我开启了探寻缓和与 <code>sync.Once</code> 关系之路。</p><p>首先我想到的是对 <code>sync.Once</code> 的二次包装，使其可以保存一个数据，这样我就可以只定义 <code>Once</code> 的实例，由 <code>Once</code> 负责存储初始化的结果。<a href="https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go#L34">exsync.Once</a> 这是我的第一个实验，它的实现非常简洁：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go</span></span><br><span class="line"><span class="keyword">type</span> Once <span class="keyword">struct</span> &#123;</span><br><span class="line">once sync.Once</span><br><span class="line">v    <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(o *Once)</span> <span class="title">Do</span><span class="params">(f <span class="keyword">func</span>()</span> <span class="title">interface</span></span>&#123;&#125;) <span class="keyword">interface</span>&#123;&#125; &#123;</span><br><span class="line">o.once.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">o.v = f()</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> o.v</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它嵌套一个 <code>sync.Once</code> 实例，并覆盖其 <code>Do</code> 函数，使其接收一个 <code>func() interface&#123;&#125;</code> 函数，它要求初始化函数返回其结果，结果保存在 <code>Once.v</code> ，每次调用 <code>Do</code> 它便返回自己保存的结果，这使用起来就变得简单许多，改造之前 <code>exec.closeOnce</code> 例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> closeOnce <span class="keyword">struct</span> &#123;</span><br><span class="line">*os.File</span><br><span class="line"></span><br><span class="line">once exsync.Once</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">Close</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> c.once.Do(c.<span class="built_in">close</span>).(error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">close</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> c.File.Close()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这减少了一个业务层的数据定义，如果包含多个数据，可以使用自定义 <code>struct</code> 或者 <code>[]interface&#123;&#125;</code> 进行数据保存， 一个简单打开文件的例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> openOnce <span class="keyword">struct</span> &#123;</span><br><span class="line">file exsync.Once</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *openOnce)</span> <span class="title">Open</span><span class="params">(name <span class="keyword">string</span>)</span> <span class="params">(*os.File, error)</span></span> &#123;</span><br><span class="line">f := c.file.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line">f, err := os.Open(name)</span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">interface</span>&#123;&#125;&#123;f, err&#125;</span><br><span class="line">&#125;).([]<span class="keyword">interface</span>&#123;&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> f[<span class="number">0</span>].(*os.File), f[<span class="number">1</span>].(error)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这看起来使初始化的代码变得复杂了一些，对多返回值的问题暂时没有更好的实现，我会在后续逐渐考虑这类问题的处理方式，单个值时它使我得到一些惊喜和便捷。即使这样我随后发现它相对 <code>sync.Once</code> 的性能大幅度下降，达到10倍之多，起初我认为是 <code>interface</code> 的带来的，我立刻实现了一个 <a href="https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go#L66">exsync.OncePointer</a> 以期许它可以在性能上给我一个惊喜：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// source: https://github.com/thinkeridea/go-extend/blob/efa13c9456cb4ce97c16824de2996c84fa285fc3/exsync/once.go</span></span><br><span class="line"><span class="keyword">type</span> OncePointer <span class="keyword">struct</span> &#123;</span><br><span class="line">once sync.Once</span><br><span class="line">v    unsafe.Pointer</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(o *OncePointer)</span> <span class="title">Do</span><span class="params">(f <span class="keyword">func</span>()</span> <span class="title">unsafe</span>.<span class="title">Pointer</span>) <span class="title">unsafe</span>.<span class="title">Pointer</span></span> &#123;</span><br><span class="line">o.once.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">o.v = f()</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> o.v</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用 <code>unsafe.Pointer</code> 存储实例，让其在编译时确定类型，来提升其性能，使用示例如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> closeOnce <span class="keyword">struct</span> &#123;</span><br><span class="line">*os.File</span><br><span class="line"></span><br><span class="line">once exsync.OncePointer</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">Close</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> *(*error)(c.once.Do(c.<span class="built_in">close</span>))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *closeOnce)</span> <span class="title">close</span><span class="params">()</span> <span class="title">unsafe</span>.<span class="title">Pointer</span></span> &#123;</span><br><span class="line">err := c.File.Close()</span><br><span class="line"><span class="keyword">return</span> unsafe.Pointer(&amp;err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>尴尬的是这并没有使其性能有极大提升，仅仅只是稍微提升一些，难道我要和 <code>sync.Once</code> 就此诀别，还是凑合过……</p><h2 id="转机的到来"><a href="#转机的到来" class="headerlink" title="转机的到来"></a>转机的到来</h2><p>我本已放弃优化，即使其性能极大下降，但是它仍然可以在 3ns 内完成任务，这并不会形成瓶颈。但多少内心还是有些不甘，仅仅只是包装使其保存一个值不应该导致性能下降如此严重，究竟是什么导致其性能如此严重下降的，仔细做了分析发现由于 <code>sync.Once</code> 非常的高效，且代码简洁，我嵌套包装使其多了一层调用，且可能导致其无法内联，这对一些性能不高的组件影响极小，但是像 <code>sync.Once</code> 这样高效任何小小的损耗表现都十分明显。</p><p>我直接拷贝 <code>sync.Once</code> 中的代码到 <a href="https://github.com/thinkeridea/go-extend/blob/main/exsync/once.go#L35">exsync.Once</a> 及 <a href="https://github.com/thinkeridea/go-extend/blob/main/exsync/once.go#L82">exsync.OncePointer</a> 实现中，这让我得到与 <code>sync.Once</code> 接近的性能，<a href="https://github.com/thinkeridea/go-extend/blob/main/exsync/once.go#L82">exsync.OncePointer</a> 的实现甚至总是好于 <code>sync.Once</code>。</p><p>以下是性能测试的结果，其代码位于 <a href="https://github.com/thinkeridea/go-extend/blob/main/exsync/benchmark/once_test.go">exsync/benchmark/once_test.go</a>:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/<span class="keyword">go</span>-extend/exsync/benchmark</span><br><span class="line">BenchmarkSyncOnce<span class="number">-8</span>      <span class="number">1000000000</span>         <span class="number">0.391</span> ns/op       <span class="number">0</span> B/op       <span class="number">0</span> allocs/op</span><br><span class="line">BenchmarkOnce<span class="number">-8</span>          <span class="number">1000000000</span>         <span class="number">0.407</span> ns/op       <span class="number">0</span> B/op       <span class="number">0</span> allocs/op</span><br><span class="line">BenchmarkOncePointer<span class="number">-8</span>   <span class="number">1000000000</span>         <span class="number">0.389</span> ns/op       <span class="number">0</span> B/op       <span class="number">0</span> allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com/thinkeridea/<span class="keyword">go</span>-extend/exsync/benchmark<span class="number">1.438</span>s</span><br></pre></td></tr></table></figure><p>得到这个结果后我毫不犹豫、马不停蹄的改变了 <a href="https://github.com/thinkeridea/go-extend/blob/main/pool/buffer.go#L24">pool.BufferPool</a> 中的代码，这使 <a href="https://github.com/thinkeridea/go-extend/blob/main/pool/buffer.go#L24">pool.BufferPool</a> 变得简洁许多：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> pool</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exsync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> (</span><br><span class="line">buff64   exsync.OncePointer</span><br><span class="line">buff128  exsync.OncePointer</span><br><span class="line">buff512  exsync.OncePointer</span><br><span class="line">buff1024 exsync.OncePointer</span><br><span class="line">buff2048 exsync.OncePointer</span><br><span class="line">buff4096 exsync.OncePointer</span><br><span class="line">buff8192 exsync.OncePointer</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> bufferPool <span class="keyword">struct</span> &#123;</span><br><span class="line">sync.Pool</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// BufferPool bytes.Buffer 的 sync.Pool 接口</span></span><br><span class="line"><span class="comment">// 可以直接 Get *bytes.Buffer 并 Reset Buffer</span></span><br><span class="line"><span class="keyword">type</span> BufferPool <span class="keyword">interface</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Get 从 Pool 中获取一个 *bytes.Buffer 实例, 该实例已经被 Reset</span></span><br><span class="line">Get() *bytes.Buffer</span><br><span class="line"><span class="comment">// Put 把 *bytes.Buffer 放回 Pool 中</span></span><br><span class="line">Put(*bytes.Buffer)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newBufferPool</span><span class="params">(size <span class="keyword">int</span>)</span> <span class="title">unsafe</span>.<span class="title">Pointer</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> unsafe.Pointer(&amp;bufferPool&#123;</span><br><span class="line">Pool: sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, size))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;,</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetBuff64 获取一个初始容量为 64 的 *bytes.Buffer Pool</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetBuff64</span><span class="params">()</span> <span class="title">BufferPool</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> (*bufferPool)(buff64.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">unsafe</span>.<span class="title">Pointer</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> newBufferPool(<span class="number">64</span>)</span><br><span class="line">&#125;))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>如此对 <code>sync.Once</code> 进行二次封装，使其通用性有所下降，并一定是一个好的方案，我乐于公开它，因为它在大多数时刻可以减少使用者的负担，使得代码变的简练。</p><p>后续的思考：</p><ul><li><code>Once</code> 永远只能执行一次，是否有安全快捷的方法可以使其重置。</li><li>出现错误时，能否提供一种重试机制，否者程序会一直无法得到正确的结果，比如建立数据库连接，某个时刻数据库出现故障，而恰恰这时首次执行了 <code>Do</code> 函数。</li><li>对多个值的调用方式上是否能提供简单的调用机制。</li></ul><p>解决以上这些问题，可以使 <code>sync.Once</code> 应用在更多的场景中，但势必导致其性能有所下降，这需要一些实验和折中处理。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;官方描述 &lt;code&gt;Once is an object that will perform exactly one action&lt;/code&gt;, 即 &lt;code&gt;Once&lt;/code&gt; 是一个对象,它提供了保证某个动作只被执行一次功能，最典型的场景就是单例模式，&lt;code&gt;Once&lt;/code&gt; 可用于任何符合 “exactly once” 语义的场景。&lt;/p&gt;
&lt;h2 id=&quot;sync-Once-的用法&quot;&gt;&lt;a href=&quot;#sync-Once-的用法&quot; class=&quot;headerlink&quot; title=&quot;sync.Once 的用法&quot;&gt;&lt;/a&gt;sync.Once 的用法&lt;/h2&gt;&lt;p&gt;在多数情况下，&lt;code&gt;sync.Once&lt;/code&gt; 被用于控制变量的初始化，这个变量的读写通常遵循单例模式，满足这三个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当且仅当第一次读某个变量时，进行初始化（写操作）&lt;/li&gt;
&lt;li&gt;变量被初始化过程中，所有读都被阻塞（读操作；当变量初始化完成后，读操作继续进行）&lt;/li&gt;
&lt;li&gt;变量仅初始化一次，初始化完成后驻留在内存里&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="unsafe" scheme="https://blog.thinkeridea.com/tags/unsafe/"/>
    
    <category term="sync" scheme="https://blog.thinkeridea.com/tags/sync/"/>
    
    <category term="once" scheme="https://blog.thinkeridea.com/tags/once/"/>
    
    <category term="OncePointer" scheme="https://blog.thinkeridea.com/tags/oncepointer/"/>
    
    <category term="BufferPool" scheme="https://blog.thinkeridea.com/tags/bufferpool/"/>
    
    <category term="strings.Replacer" scheme="https://blog.thinkeridea.com/tags/strings-replacer/"/>
    
    <category term="closeOnce" scheme="https://blog.thinkeridea.com/tags/closeonce/"/>
    
  </entry>
  
  <entry>
    <title>【Go】高效截取字符串的一些思考</title>
    <link href="https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html"/>
    <id>https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html</id>
    <published>2019-10-30T22:35:34.000Z</published>
    <updated>2019-12-10T06:53:28.520Z</updated>
    
    <content type="html"><![CDATA[<p>最近我在 <a href="https://forum.golangbridge.org/">Go Forum</a> 中发现了 <a href="https://forum.golangbridge.org/t/solved-string-size-of-20-character/15783">[SOLVED] String size of 20 character</a> 的问题，“<strong>hollowaykeanho</strong>” 给出了相关的答案，而我从中发现了截取字符串的方案并非最理想的方法，因此做了一系列实验并获得高效截取字符串的方法，这篇文章将逐步讲解我实践的过程。</p><h2 id="字节切片截取"><a href="#字节切片截取" class="headerlink" title="字节切片截取"></a>字节切片截取</h2><p>这正是 “<strong>hollowaykeanho</strong>” 给出的第一个方案，我想也是很多人想到的第一个方案，利用 go 的内置切片语法截取字符串：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">s := <span class="string">&quot;abcdef&quot;</span></span><br><span class="line">fmt.Println(s[<span class="number">1</span>:<span class="number">4</span>])</span><br></pre></td></tr></table></figure><p>我们很快就了解到这是按字节截取，在处理 <code>ASCII</code> 单字节字符串截取，没有什么比这更完美的方案了，中文往往占多个字节，在 <code>utf8</code> 编码中是3个字节，如下程序我们将获得乱码数据：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">s := <span class="string">&quot;Go 语言&quot;</span></span><br><span class="line">fmt.Println(s[<span class="number">1</span>:<span class="number">4</span>])</span><br></pre></td></tr></table></figure><a id="more"></a><h2 id="杀手锏-类型转换-rune"><a href="#杀手锏-类型转换-rune" class="headerlink" title="杀手锏 - 类型转换 []rune"></a>杀手锏 - 类型转换 []rune</h2><p>“<strong>hollowaykeanho</strong>”  给出的第二个方案就是将字符串转换为 <code>[]rune</code>，然后按切片语法截取，再把结果转成字符串。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">s := <span class="string">&quot;Go 语言&quot;</span></span><br><span class="line">rs := []<span class="keyword">rune</span>(s)</span><br><span class="line">fmt.Println(strings(rs[<span class="number">1</span>:<span class="number">4</span>]))</span><br></pre></td></tr></table></figure><p>首先我们得到了正确的结果，这是最大的进步。不过我对类型转换一直比较谨慎，我担心它的性能问题，因此我尝试在搜索引擎和各大论坛查找答案，但是我得到最多的还是这个方案，似乎这已经是唯一的解。</p><p>我尝试写个性能测试评测它的性能：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> benchmark</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> benchmarkSubString = <span class="string">&quot;Go语言是Google开发的一种静态强类型、编译型、并发型，并具有垃圾回收功能的编程语言。为了方便搜索和识别，有时会将其称为Golang。&quot;</span></span><br><span class="line"><span class="keyword">var</span> benchmarkSubStringLength = <span class="number">20</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SubStrRunes</span><span class="params">(s <span class="keyword">string</span>, length <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> utf8.RuneCountInString(s) &gt; length &#123;</span><br><span class="line">rs := []<span class="keyword">rune</span>(s)</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">string</span>(rs[:length])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkSubStrRunes</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">SubStrRunes(benchmarkSubString, benchmarkSubStringLength)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我得到了让我有些吃惊的结果：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark</span><br><span class="line">BenchmarkSubStrRunes-8            872253              1363 ns/op             336 B/op          2 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     2.120s</span><br></pre></td></tr></table></figure><p>对 69 个的字符串截取前 20 个字符需要大概 1.3 微秒，这极大的超出了我的心里预期，我发现因为类型转换带来了内存分配，这产生了一个新的字符串，并且类型转换需要大量的计算。</p><h2 id="救命稻草-utf8-DecodeRuneInString"><a href="#救命稻草-utf8-DecodeRuneInString" class="headerlink" title="救命稻草 - utf8.DecodeRuneInString"></a>救命稻草 - utf8.DecodeRuneInString</h2><p>我想改善类型转换带来的额外运算和内存分配，我仔细的梳理了一遍 <code>strings</code> 包，发现并没有相关的工具，这时我想到了 <code>utf8</code> 包，它提供了多字节计算相关的工具，实话说我对它并不熟悉，或者说没有主动（直接）使用过它，我查看了它所有的文档发现 <code>utf8.DecodeRuneInString</code> 函数可以转换单个字符，并给出字符占用字节的数量，我尝试了如此下的实验：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> benchmark</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"><span class="string">&quot;unicode/utf8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> benchmarkSubString = <span class="string">&quot;Go语言是Google开发的一种静态强类型、编译型、并发型，并具有垃圾回收功能的编程语言。为了方便搜索和识别，有时会将其称为Golang。&quot;</span></span><br><span class="line"><span class="keyword">var</span> benchmarkSubStringLength = <span class="number">20</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SubStrDecodeRuneInString</span><span class="params">(s <span class="keyword">string</span>, length <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> size, n <span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; length &amp;&amp; n &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">_, size = utf8.DecodeRuneInString(s[n:])</span><br><span class="line">n += size</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> s[:n]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkSubStrDecodeRuneInString</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">SubStrDecodeRuneInString(benchmarkSubString, benchmarkSubStringLength)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行它之后我得到了令我惊喜的结果：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark</span><br><span class="line">BenchmarkSubStrDecodeRuneInString-8     10774401               105 ns/op               0 B/op          0 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.250s</span><br></pre></td></tr></table></figure><p>较 <code>[]rune</code> 类型转换效率提升了 <strong>13倍</strong>，消除了内存分配，它的确令人激动和兴奋，我迫不及待的回复了 “<strong>hollowaykeanho</strong>” 告诉他我发现了一个更好的方法，并提供了相关的性能测试。</p><p>我有些小激动，兴奋的浏览着论坛里各种有趣的问题，在查看一个问题的帮助时 (忘记是哪个问题了-_-||) ，我惊奇的发现了另一个思路。</p><h2 id="良药不一定苦-range-字符串迭代"><a href="#良药不一定苦-range-字符串迭代" class="headerlink" title="良药不一定苦 - range 字符串迭代"></a>良药不一定苦 - range 字符串迭代</h2><p>许多人似乎遗忘了 <code>range</code> 是按字符迭代的，并非字节。使用 <code>range</code> 迭代字符串时返回字符起始索引和对应的字符，我立刻尝试利用这个特性编写了如下用例：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">package benchmark</span><br><span class="line"></span><br><span class="line">import (</span><br><span class="line">&quot;testing&quot;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">var benchmarkSubString = &quot;Go语言是Google开发的一种静态强类型、编译型、并发型，并具有垃圾回收功能的编程语言。为了方便搜索和识别，有时会将其称为Golang。&quot;</span><br><span class="line">var benchmarkSubStringLength = 20</span><br><span class="line"></span><br><span class="line">func SubStrRange(s string, length int) string &#123;</span><br><span class="line">var n, i int</span><br><span class="line">for i = range s &#123;</span><br><span class="line">if n == length &#123;</span><br><span class="line">break</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">n++</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">return s[:i]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">func BenchmarkSubStrRange(b *testing.B) &#123;</span><br><span class="line">for i := 0; i &lt; b.N; i++ &#123;</span><br><span class="line">SubStrRange(benchmarkSubString, benchmarkSubStringLength)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我尝试运行它，这似乎有着无穷的魔力，结果并没有令我失望。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark</span><br><span class="line">BenchmarkSubStrRange-8          12354991                91.3 ns/op             0 B/op          0 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.233s</span><br></pre></td></tr></table></figure><p>它仅仅提升了13%，但它足够的简单和易于理解，这似乎就是我苦苦寻找的那味良药。</p><p>如果你以为这就结束了，不、这对我来只是探索的开始。</p><h2 id="终极时刻-自己造轮子"><a href="#终极时刻-自己造轮子" class="headerlink" title="终极时刻 - 自己造轮子"></a>终极时刻 - 自己造轮子</h2><p>喝了 <code>range</code> 那碗甜的腻人的良药，我似乎冷静下来了，我需要造一个轮子，它需要更易用，更高效。</p><p>于是乎我仔细观察了两个优化方案，它们似乎都是为了查找截取指定长度字符的索引位置，如果我可以提供一个这样的方法，是否就可以提供用户一个简单的截取实现 <code>s[:strIndex(20)]</code> ，这个想法萌芽之后我就无法再度摆脱，我苦苦思索两天来如何来提供易于使用的接口。</p><p>之后我创造了 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneIndexInString">exutf8.RuneIndexInString</a> 和 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneIndex">exutf8.RuneIndex</a> 方法，分别用来计算字符串和字节切片中指定字符数量结束的索引位置。</p><p>我用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneIndexInString">exutf8.RuneIndexInString</a>  实现了一个字符串截取测试：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> benchmark</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"><span class="string">&quot;unicode/utf8&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exunicode/exutf8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> benchmarkSubString = <span class="string">&quot;Go语言是Google开发的一种静态强类型、编译型、并发型，并具有垃圾回收功能的编程语言。为了方便搜索和识别，有时会将其称为Golang。&quot;</span></span><br><span class="line"><span class="keyword">var</span> benchmarkSubStringLength = <span class="number">20</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SubStrRuneIndexInString</span><span class="params">(s <span class="keyword">string</span>, length <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">n, _ := exutf8.RuneIndexInString(s, length)</span><br><span class="line"><span class="keyword">return</span> s[:n]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkSubStrRuneIndexInString</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">SubStrRuneIndexInString(benchmarkSubString, benchmarkSubStringLength)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>尝试运行它，我对结果感到十分欣慰：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark</span><br><span class="line">BenchmarkSubStrRuneIndexInString-8      13546849                82.4 ns/op             0 B/op          0 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.213s</span><br></pre></td></tr></table></figure><p>性能较 <code>range</code> 提升了 10%，让我很欣慰可以再次获得新的提升，这证明它是有效的。</p><p>它足够的高效，但是却不够易用，我截取字符串需要两行代码，如果我想截取 10~20之间的字符就需要4行代码，这并不是用户易于使用的接口，我参考了其它语言的 <code>sub_string</code> 方法，我想我应该也设计一个这个样的接口给用户。</p><p><a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneSubString">exutf8.RuneSubString</a> 和 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneSub">exutf8.RuneSub</a> 是我认真思索后编写的方法：</p><p><code>func RuneSubString(s string, start, length int) string</code></p><p>它有三个参数：</p><ul><li><code>s</code>  : 输入的字符串 </li><li><code>start</code> : 开始截取的位置，如果 start 是非负数，返回的字符串将从 string 的 start 位置开始，从 0 开始计算。例如，在字符串 “abcdef” 中，在位置 0 的字符是 “a”，位置 2 的字符串是 “c” 等等。 如果 start 是负数，返回的字符串将从 string 结尾处向前数第 start 个字符开始。 如果 string 的长度小于 start，将返回空字符串。</li><li><code>length</code>：截取的长度，如果提供了正数的 length，返回的字符串将从 start 处开始最多包括 length 个字符（取决于 string 的长度）。 如果提供了负数的 length，那么 string 末尾处的 length 个字符将会被省略（若 start 是负数则从字符串尾部算起）。如果 start 不在这段文本中，那么将返回空字符串。 如果提供了值为 0 的 length，返回的子字符串将从 start 位置开始直到字符串结尾。</li></ul><p>我为他们提供了别名，根据使用习惯大家更倾向去 <code>strings</code> 包寻找这类问题的解决方法，我创建了<a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings#SubString">exstrings.SubString</a> 和 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#Sub">exbytes.Sub</a> 作为更易检索到的别名方法。</p><p>最后我需要再做一个性能测试，确保它的性能：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> benchmark</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exunicode/exutf8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> benchmarkSubString = <span class="string">&quot;Go语言是Google开发的一种静态强类型、编译型、并发型，并具有垃圾回收功能的编程语言。为了方便搜索和识别，有时会将其称为Golang。&quot;</span></span><br><span class="line"><span class="keyword">var</span> benchmarkSubStringLength = <span class="number">20</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SubStrRuneSubString</span><span class="params">(s <span class="keyword">string</span>, length <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> exutf8.RuneSubString(s, <span class="number">0</span>, length)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkSubStrRuneSubString</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">SubStrRuneSubString(benchmarkSubString, benchmarkSubStringLength)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行它，不会让我失望：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark</span><br><span class="line">BenchmarkSubStrRuneSubString-8          13309082                83.9 ns/op             0 B/op          0 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.215s</span><br></pre></td></tr></table></figure><p>虽然相较 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneIndexInString">exutf8.RuneIndexInString</a> 有所下降，但它提供了易于交互和使用的接口，我认为这应该是最实用的方案，如果你追求极致仍然可以使用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exunicode/exutf8#RuneIndexInString">exutf8.RuneIndexInString</a>，它依然是最快的方案。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>当看到有疑问的代码，即使它十分的简单，依然值得深究，并不停的探索它，这并不枯燥和乏味，反而会有极多收获。</p><p>从起初 <code>[]rune</code> 类型转换到最后自己造轮子，不仅得到了<strong>16倍</strong>的性能提升，我还学习了<code>utf8</code>包、加深了<code>range</code> 遍历字符串的特性 以及为 <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 仓库收录了多个实用高效的解决方案，让更多  <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 的用户得到成果。</p><p> <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 是一个收录实用、高效方法的仓库，读者们如果好的函数和通用高效的解决方案，期待你们不吝啬给我发送 <code>Pull request</code>，你也可以使用这个仓库加快功能实现及提升性能。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近我在 &lt;a href=&quot;https://forum.golangbridge.org/&quot;&gt;Go Forum&lt;/a&gt; 中发现了 &lt;a href=&quot;https://forum.golangbridge.org/t/solved-string-size-of-20-character/15783&quot;&gt;[SOLVED] String size of 20 character&lt;/a&gt; 的问题，“&lt;strong&gt;hollowaykeanho&lt;/strong&gt;” 给出了相关的答案，而我从中发现了截取字符串的方案并非最理想的方法，因此做了一系列实验并获得高效截取字符串的方法，这篇文章将逐步讲解我实践的过程。&lt;/p&gt;
&lt;h2 id=&quot;字节切片截取&quot;&gt;&lt;a href=&quot;#字节切片截取&quot; class=&quot;headerlink&quot; title=&quot;字节切片截取&quot;&gt;&lt;/a&gt;字节切片截取&lt;/h2&gt;&lt;p&gt;这正是 “&lt;strong&gt;hollowaykeanho&lt;/strong&gt;” 给出的第一个方案，我想也是很多人想到的第一个方案，利用 go 的内置切片语法截取字符串：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;s := &lt;span class=&quot;string&quot;&gt;&amp;quot;abcdef&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;fmt.Println(s[&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;:&lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;])&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;我们很快就了解到这是按字节截取，在处理 &lt;code&gt;ASCII&lt;/code&gt; 单字节字符串截取，没有什么比这更完美的方案了，中文往往占多个字节，在 &lt;code&gt;utf8&lt;/code&gt; 编码中是3个字节，如下程序我们将获得乱码数据：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;s := &lt;span class=&quot;string&quot;&gt;&amp;quot;Go 语言&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;fmt.Println(s[&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;:&lt;span class=&quot;number&quot;&gt;4&lt;/span&gt;])&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="substring" scheme="https://blog.thinkeridea.com/tags/substring/"/>
    
    <category term="substr" scheme="https://blog.thinkeridea.com/tags/substr/"/>
    
    <category term="utf8" scheme="https://blog.thinkeridea.com/tags/utf8/"/>
    
    <category term="中文字符串截取" scheme="https://blog.thinkeridea.com/tags/%E4%B8%AD%E6%96%87%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%88%AA%E5%8F%96/"/>
    
  </entry>
  
  <entry>
    <title>【ChatOps系列】GitLab系列环境准备</title>
    <link href="https://blog.thinkeridea.com/201907/chatops/gitlab_environmental_preparation.html"/>
    <id>https://blog.thinkeridea.com/201907/chatops/gitlab_environmental_preparation.html</id>
    <published>2019-07-27T02:18:01.000Z</published>
    <updated>2019-07-27T11:34:02.998Z</updated>
    
    <content type="html"><![CDATA[<p>在搭建 GitLab 系列 ChatOps 时需要掌握一些基础的技能知识，整个 ChatOps 涉及到很多方面，我们至少需要掌握以下技能：</p><ul><li>Linux 示例中我们主要以 CentOS 7 为主，但是容器内的系统会多种多样</li><li>Docker 所有的环境都整合在 Docker 容器中，这能简化搭建的过程以及提升交流的效率</li><li>Docker Compose 所有的容器我们均使用 Docker Compose 进行构建，很方便分享</li><li>Python 这个很重要，不能忽视它，整个自动部署均是 Pyhon 实现</li><li>Fabric 这个是 Python 基于 SSH 的应用部署和系统管理的库，我们基于这库实现从部署机到服务器</li><li>JavaScript/CoffeeScript 机器人 hubot 需要这些语言知识来扩展，当然还有一些其它地方会用到这些。</li><li>VMware/VirtualBox 需要搭建本地虚拟机模拟环境，示例使用 VirtualBox 演示</li></ul><a id="more"></a><p>需要掌握的技能会很多，如果你已经掌握，那么跟着教程可以很快的完成 ChatOps 的部署，如果还没有掌握，可以找时间简单学习一下。其中一些组件是可以替换的，比如 ：Fabric 就有很多选择，例如：Ansible、Shell 等可以用来操作远程机器的工具。当然也可以使用机器人连接 Jenkins、Kubernetes 等支持自动部署的服务，本系列中使用 <code>hubot-gitlab-deploy</code> 结合 <code>Fabric</code> 来实现自动部署。</p><h2 id="虚拟机搭建"><a href="#虚拟机搭建" class="headerlink" title="虚拟机搭建"></a>虚拟机搭建</h2><p>我下载的是 <a href="http://isoredirect.centos.org/centos/7/isos/x86_64/CentOS-7-x86_64-Minimal-1810.iso">CentOS7 Minimal ISO</a> 版本的镜像，如果对 linux 比较熟悉，那么安装任何版本系统都是可以的，这里并不详细讲解 Liunx 的全部安装过程，只讲解虚拟机配置及系统相关配置，供大家搭建时做参考。</p><p>VirtualBox 分配 4G 内存、500G 磁盘，同时添加两块网卡，一个是 Host-Only 模式，一个是 NAT 模式。</p><p>Host-Only 模式网卡可以给主机和虚拟机之间提供私有的虚拟网络，我们实验环境需要一个稳定的不受实际网络影响的虚拟网络，我是笔记本经常办公司到家里，网络环境会改变，但是我主机和虚拟机通信网络不会变化。</p><p>NAT 模式网卡是为了虚拟机可以上网，实际 Host-Only 的网卡也可以上网，但是它只能是使用有线连接时可以，我的笔记本大多数都是 WiFi 连接网络，这导致我没有办法上网，增加这块网卡可以实现上网，但是主机无法访问虚拟机，所以我需要两块网卡结合使用。</p><p>大家只需要能够让主机访问虚拟机，虚拟机可以访问虚拟机，虚拟机可以访问网络（我们需要安装一些东西）。</p><p><img src="/assets/image/201907/20190727165524385.png" alt="VirtualBox详细配置"></p><p>系统分区可以根据自己的喜好进行，我尽量模拟真实的生成环境，一个较小的系统盘，阿里云服务器是 40G，一块数据盘，我把其它容量都分配给 <code>/data</code> 数据盘。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> df -h</span></span><br><span class="line">文件系统        容量  已用  可用 已用% 挂载点</span><br><span class="line">/dev/sda1        40G  1.2G   39G    3% /  # 系统盘</span><br><span class="line">devtmpfs        1.9G     0  1.9G    0% /dev</span><br><span class="line">tmpfs           1.9G     0  1.9G    0% /dev/shm</span><br><span class="line">tmpfs           1.9G  8.5M  1.9G    1% /run</span><br><span class="line">tmpfs           1.9G     0  1.9G    0% /sys/fs/cgroup</span><br><span class="line">/dev/sda3       452G   33M  452G    1% /data  # 数据盘</span><br><span class="line">tmpfs           379M     0  379M    0% /run/user/0</span><br></pre></td></tr></table></figure><p>安装完成系统，登陆之后需要给虚拟机设置一个固定 ip。使用 <code>ip address</code> 查看网卡信息，找到 <code>192.168.*.*</code> 的网卡，记录下网卡名称，我的是 <code>enp0s3</code>，然后编辑网卡对应的配置文件 <code>/etc/sysconfig/network-scripts/ifcfg-enp0s3</code>，具体如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">TYPE=&quot;Ethernet&quot;</span><br><span class="line">PROXY_METHOD=&quot;none&quot;</span><br><span class="line">BROWSER_ONLY=&quot;no&quot;</span><br><span class="line">BOOTPROTO=&quot;static&quot;  # 使用静态网络</span><br><span class="line">DEFROUTE=&quot;yes&quot;</span><br><span class="line">IPV4_FAILURE_FATAL=&quot;no&quot;</span><br><span class="line">IPV6INIT=&quot;yes&quot;</span><br><span class="line">IPV6_AUTOCONF=&quot;yes&quot;</span><br><span class="line">IPV6_DEFROUTE=&quot;yes&quot;</span><br><span class="line">IPV6_FAILURE_FATAL=&quot;no&quot;</span><br><span class="line">IPV6_ADDR_GEN_MODE=&quot;stable-privacy&quot;</span><br><span class="line">NAME=&quot;enp0s3&quot;</span><br><span class="line">UUID=&quot;6f19c6e8-87de-42a5-afdb-2fe8a8bc74a8&quot;</span><br><span class="line">DEVICE=&quot;enp0s3&quot;</span><br><span class="line">ONBOOT=&quot;yes&quot; # 开机启动</span><br><span class="line">IPADDR=192.168.56.6  # 网络地址，根据之前查询的网段分配</span><br><span class="line">NETMASK=255.255.255.0 # 子网掩码</span><br><span class="line"><span class="meta">#</span><span class="bash">GATEWAY=192.168.56.1 <span class="comment">#默认网关，如果和我的网络配置一致就不用配置了</span></span></span><br><span class="line"><span class="meta">#</span><span class="bash">DNS1=192.168.7.1 <span class="comment">#DNS 配置，如果和我的网络配置一致就不用配置了</span></span></span><br></pre></td></tr></table></figure><p>重启网卡 <code>systemctl restart network </code> 使配置生效，然后在主机使用 ssh 登陆虚拟机，确定网络畅通，<code>ssh root@192.168.56.6</code>。</p><p>之后我们就可以使用 “无界面” 启动虚拟机，使用 ssh 登陆虚拟机进行配置。</p><h2 id="Docker-环境安装"><a href="#Docker-环境安装" class="headerlink" title="Docker 环境安装"></a>Docker 环境安装</h2><p>官网有完整的安装教程在：<a href="https://docs.docker.com/install/linux/docker-ce/centos/">https://docs.docker.com/install/linux/docker-ce/centos/</a> ，有两种安装方式，我使用在线安装的方式，由于官方镜像无法访问，这里采用阿里云的镜像，只需要执行以下命令即可：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> yum install -y yum-utils device-mapper-persistent-data lvm2</span></span><br><span class="line">......</span><br><span class="line"><span class="meta">$</span><span class="bash"> yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo</span></span><br><span class="line">......</span><br><span class="line"><span class="meta">$</span><span class="bash"> yum install docker-ce docker-ce-cli containerd.io</span></span><br><span class="line">......</span><br><span class="line"><span class="meta">$</span><span class="bash"> systemctl <span class="built_in">enable</span> docker</span></span><br><span class="line">......</span><br><span class="line"><span class="meta">$</span><span class="bash"> systemctl start docker</span></span><br><span class="line">......</span><br><span class="line"><span class="meta">$</span><span class="bash"> docker version</span></span><br><span class="line">Client: Docker Engine - Community</span><br><span class="line"> Version:           19.03.1</span><br><span class="line"> API version:       1.40</span><br><span class="line"> Go version:        go1.12.5</span><br><span class="line"> Git commit:        74b1e89</span><br><span class="line"> Built:             Thu Jul 25 21:21:07 2019</span><br><span class="line"> OS/Arch:           linux/amd64</span><br><span class="line"> Experimental:      false</span><br><span class="line"></span><br><span class="line">Server: Docker Engine - Community</span><br><span class="line"> Engine:</span><br><span class="line">  Version:          19.03.1</span><br><span class="line">  API version:      1.40 (minimum version 1.12)</span><br><span class="line">  Go version:       go1.12.5</span><br><span class="line">  Git commit:       74b1e89</span><br><span class="line">  Built:            Thu Jul 25 21:19:36 2019</span><br><span class="line">  OS/Arch:          linux/amd64</span><br><span class="line">  Experimental:     false</span><br><span class="line"> containerd:</span><br><span class="line">  Version:          1.2.6</span><br><span class="line">  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb</span><br><span class="line"> runc:</span><br><span class="line">  Version:          1.0.0-rc8</span><br><span class="line">  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f</span><br><span class="line"> docker-init:</span><br><span class="line">  Version:          0.18.0</span><br><span class="line">  GitCommit:        fec3683</span><br></pre></td></tr></table></figure><p>Docker 已经安装完成了，但是还需要配置一下，首先建议去阿里云申请一个 Docker 镜像加速器地址，去 <a href="https://links.jianshu.com/go?to=https://cr.console.aliyun.com/undefined/instances/mirrors">https://cr.console.aliyun.com/undefined/instances/mirrors</a>  申请，然后配置 <code>/etc/docker/daemon.json</code> 文件：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> vi /etc/docker/daemon.json</span></span><br><span class="line">&#123;</span><br><span class="line">&quot;data-root&quot;: &quot;/data/docker&quot;,</span><br><span class="line">&quot;registry-mirrors&quot;: [</span><br><span class="line">&quot;https://yourcode.mirror.aliyuncs.com&quot; # 替换成你申请到地址</span><br><span class="line">]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">$</span><span class="bash"> make /data/docker</span></span><br><span class="line"><span class="meta">$</span><span class="bash"> systemctl restart docker</span></span><br></pre></td></tr></table></figure><h2 id="Docker-Compose-安装"><a href="#Docker-Compose-安装" class="headerlink" title="Docker Compose 安装"></a>Docker Compose 安装</h2><p>相关的文档在 <a href="https://docs.docker.com/compose/install/">https://docs.docker.com/compose/install/</a> ，安装比较简单，执行下面的命令即可：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> curl -L <span class="string">&quot;https://github.com/docker/compose/releases/download/1.24.1/docker-compose-<span class="subst">$(uname -s)</span>-<span class="subst">$(uname -m)</span>&quot;</span> -o /usr/<span class="built_in">local</span>/bin/docker-compose</span></span><br><span class="line"></span><br><span class="line"><span class="meta">$</span><span class="bash"> chmod +x /usr/<span class="built_in">local</span>/bin/docker-compose</span></span><br><span class="line"></span><br><span class="line"><span class="meta">$</span><span class="bash"> docker-compose version</span></span><br><span class="line">docker-compose version 1.24.1, build 4667896b</span><br><span class="line">docker-py version: 3.7.3</span><br><span class="line">CPython version: 3.6.8</span><br><span class="line">OpenSSL version: OpenSSL 1.1.0j  20 Nov 2018</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>基础环境配置还是比较简单的，配置过程中遇到任何问题多 google 或者在文末留言。</p><p>整个体系涉及的技术点比较宽泛，学习者需要多准备和学习。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在搭建 GitLab 系列 ChatOps 时需要掌握一些基础的技能知识，整个 ChatOps 涉及到很多方面，我们至少需要掌握以下技能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 示例中我们主要以 CentOS 7 为主，但是容器内的系统会多种多样&lt;/li&gt;
&lt;li&gt;Docker 所有的环境都整合在 Docker 容器中，这能简化搭建的过程以及提升交流的效率&lt;/li&gt;
&lt;li&gt;Docker Compose 所有的容器我们均使用 Docker Compose 进行构建，很方便分享&lt;/li&gt;
&lt;li&gt;Python 这个很重要，不能忽视它，整个自动部署均是 Pyhon 实现&lt;/li&gt;
&lt;li&gt;Fabric 这个是 Python 基于 SSH 的应用部署和系统管理的库，我们基于这库实现从部署机到服务器&lt;/li&gt;
&lt;li&gt;JavaScript/CoffeeScript 机器人 hubot 需要这些语言知识来扩展，当然还有一些其它地方会用到这些。&lt;/li&gt;
&lt;li&gt;VMware/VirtualBox 需要搭建本地虚拟机模拟环境，示例使用 VirtualBox 演示&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="chatops" scheme="https://blog.thinkeridea.com/categories/chatops/"/>
    
    
    <category term="chatops" scheme="https://blog.thinkeridea.com/tags/chatops/"/>
    
    <category term="devops" scheme="https://blog.thinkeridea.com/tags/devops/"/>
    
    <category term="gitlab" scheme="https://blog.thinkeridea.com/tags/gitlab/"/>
    
    <category term="linux" scheme="https://blog.thinkeridea.com/tags/linux/"/>
    
    <category term="centos" scheme="https://blog.thinkeridea.com/tags/centos/"/>
    
    <category term="VirtualBox" scheme="https://blog.thinkeridea.com/tags/virtualbox/"/>
    
  </entry>
  
  <entry>
    <title>【ChatOps系列】ChatOps简介</title>
    <link href="https://blog.thinkeridea.com/201907/chatops/introduction.html"/>
    <id>https://blog.thinkeridea.com/201907/chatops/introduction.html</id>
    <published>2019-07-21T08:17:01.000Z</published>
    <updated>2019-07-21T09:28:25.361Z</updated>
    
    <content type="html"><![CDATA[<p>要说 ChatOps 就不得不说 DevOps，DevOps 是来源于 Development 和 Operations 的一个组合词，顾名思义，是一系列过程、方法与系统的统称，旨在促进开发、测试和运维人员之间的沟通与协作。简单来说，是通过引入一系列的「工具」，通过三种不同角色的开发成员间的「协作」而实现的一种「自动化」的工作模式。这种工作方式带来的好处显而易见：</p><ul><li>实现持续快速交付</li><li>能够降低人力成本</li></ul><p>但很大程度上，DevOps 更多是指开发群体之间的一种协作模式（通常也在开发人员中实施），随着全行业的发展和人力成本的攀升，在团队所有角色间贯通的升级版「DevOps」逐渐登场，也就是我们将要重点介绍的 ChatOps。</p><p><img src="/assets/image/201907/devops.png" alt="devops"></p><a id="more"></a><h2 id="ChatOps介绍"><a href="#ChatOps介绍" class="headerlink" title="ChatOps介绍"></a>ChatOps介绍</h2><p>ChatOps 的历史相对短暂，2013 年 GitHub 内部最早开始推行 ChatOps，希望能以聊天的方式更容易更快速的去完成 DevOps 承载的工作。</p><p>ChatOps 以聊天室，即沟通平台为中心，通过一系列的机器人去对接后台的各种服务，工作人员只需要在聊天窗口中与机器人对话，即可与后台服务进行交互，整个工作的展开就像是使唤一个智能助手那样简单自然。</p><p><img src="/assets/image/201907/chatops_devops.png" alt="chatops_devops"></p><p>ChatOps 站在巨人的肩膀上发展，也为工作带来了显而易见的好处：</p><ul><li>公开透明。所有的工作消息都在同一个聊天平台中沉淀并公开给所有相关成员，消除沟通壁垒，工作历史有迹可循，团队合作更加顺畅。</li><li>上下文共享。减少因工作台切换等对消息的截断，保证消息的完整性，让工作承接有序，各角色，各工具都成为完成工作流中的一环，打造真正流畅的工作体验。</li><li>移动友好。只需要在前台与预设好的机器人对话即可完成与后台工具、系统的交互，在移动环境下无需再与众多复杂的工具直接对接，大大提升移动办公的可行性。</li><li>DevOps 文化打造。用与机器人对话这种简单的方式降低 DevOps 的接受门槛，让这种自动化办公的理念更容易的扩展到团队的每一个角落。</li></ul><p><img src="/assets/image/201907/chatops.png" alt="chatops"></p><p>ChatOps 主要由三个部分构成：聊天室（控制中心）、机器人（连接中心）、基础设施，基础设施主要是支撑我们业务运行的各种服务与工具，在构建 ChatOps 时主要需要选择聊天室和机器人，国外早期的工作沟通工具 HipChat，新秀 Slack 都是作为 ChatOps 承载平台的好选择，在中文的环境下，则可以选择 BearyChat（倍洽）等等。</p><p>聊天室选择：</p><ul><li>功能完备</li><li>操作交互友好</li><li>多平台支持</li><li>API 丰富</li><li>三方工具支持丰富</li><li>机器人支持程度</li></ul><p>机器人选择：</p><ul><li>功能完备</li><li>易扩展（我们可能需要根据基础设施编写大量机器人扩展）</li><li>编程语言熟悉（因为我们需要编写扩展）</li><li>开源扩展丰富（可以极大减少开发工作量）</li></ul><p>聊天室主要有：Slack、HipChat、BearyChat（倍洽）、Rocket.Chat、钉钉（机器人支持程度不够，不太支持）。机器人主要有：Hubot（javascript/CoffeeScript）、Lita（Ruby），Errbot（python）。</p><p>机器人我推荐使用 Hubot，后面所有的实验都使用 Hubot 展开。 GitHub 团队内部实现的 ChatOps 与一个叫做 Hubot 的机器人框架密切相关，Hubot 提供很多聊天机器人所需的基础设施，借助 Hubot 框架能比较方便的和自己编写的功能或自己的系统对接。目前，Hubot 已经发展出了较好的生态圈，有很多开源插件可以借用。</p><p><img src="/assets/image/201907/hubot.png" alt="chatops"></p><h2 id="ChatOps实现"><a href="#ChatOps实现" class="headerlink" title="ChatOps实现"></a>ChatOps实现</h2><p>本系列专题主要讲两种实现，一种是基于 GitHub+Hubot+Slack 实现，一种基于开源体系的 GitLab + Hubot + Rocket.Chat，它们功能完善且有良好的扩展及丰富的API，只要爱倒腾一定会有意想不到的收获。</p><p>GitHub 体系：</p><ul><li>slack  聊天室</li><li>Hubot  机器人</li><li>GitHub 代码管理</li><li>Travis CI 自动化测试</li><li>heaven 自动部署</li><li>hubot-deploy 连接 Hubot 与 heaven</li></ul><p>GitLab 体系：</p><ul><li>Recket.chat 聊天室</li><li>Hubot  机器人</li><li>GitLab 代码管理</li><li>GitLab CI 自动化测试</li><li>hubot-gitlab-deploy 自动化部署（计划重构中）</li><li>fabric-deploy-script 基于配置的 fabric 自动部署实现 (计划开源中)</li></ul><p>本系列会介绍各种设计服务安装、配置，以及各种服务组件之间连接配置等，同时会涉及到项目配置管理、密钥管理等相关知识。</p><h2 id="计划"><a href="#计划" class="headerlink" title="计划"></a>计划</h2><p>本系列主要涉及 ChatOps 环境搭建、工具配置，以及项目的持续集成、持续部署的实现，持续部署过程会涉及到密钥管理、配置管理等等。</p><p>本系列计划一至两周一篇文章，每篇文章介绍一个点，两种体系都会讲到，大约20~30篇文章，优先讲解开源体系的 GitLab 篇。</p><p>在这期间会开发相关机器人脚本及相关服务组件，可以形成项目会发布在 GitHub 上，不能形成项目的会在文章中，最后所有实验相关代码均可在 GitHub 上 <a href="https://github.com/thinkeridea/chatops_experiment">chatops_experiment</a> 中获取。</p><p>测试中需要自动部署的项目会单独存储在一个账号/组织下面，具体详见每个章节。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;要说 ChatOps 就不得不说 DevOps，DevOps 是来源于 Development 和 Operations 的一个组合词，顾名思义，是一系列过程、方法与系统的统称，旨在促进开发、测试和运维人员之间的沟通与协作。简单来说，是通过引入一系列的「工具」，通过三种不同角色的开发成员间的「协作」而实现的一种「自动化」的工作模式。这种工作方式带来的好处显而易见：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现持续快速交付&lt;/li&gt;
&lt;li&gt;能够降低人力成本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但很大程度上，DevOps 更多是指开发群体之间的一种协作模式（通常也在开发人员中实施），随着全行业的发展和人力成本的攀升，在团队所有角色间贯通的升级版「DevOps」逐渐登场，也就是我们将要重点介绍的 ChatOps。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/image/201907/devops.png&quot; alt=&quot;devops&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="chatops" scheme="https://blog.thinkeridea.com/categories/chatops/"/>
    
    
    <category term="chatops" scheme="https://blog.thinkeridea.com/tags/chatops/"/>
    
    <category term="devops" scheme="https://blog.thinkeridea.com/tags/devops/"/>
    
    <category term="hubot" scheme="https://blog.thinkeridea.com/tags/hubot/"/>
    
    <category term="github" scheme="https://blog.thinkeridea.com/tags/github/"/>
    
    <category term="gitlab" scheme="https://blog.thinkeridea.com/tags/gitlab/"/>
    
    <category term="slack" scheme="https://blog.thinkeridea.com/tags/slack/"/>
    
    <category term="rocket.chat" scheme="https://blog.thinkeridea.com/tags/rocket-chat/"/>
    
  </entry>
  
  <entry>
    <title>【Go】类似csv的数据日志组件设计</title>
    <link href="https://blog.thinkeridea.com/201907/go/csv_like_data_logs.html"/>
    <id>https://blog.thinkeridea.com/201907/go/csv_like_data_logs.html</id>
    <published>2019-07-16T22:52:34.000Z</published>
    <updated>2019-10-30T22:43:57.418Z</updated>
    
    <content type="html"><![CDATA[<p>我们业务每天需要记录大量的日志数据，且这些数据十分重要，它们是公司收入结算的主要依据，也是数据分析部门主要得数据源，针对这么重要的日志，且高频率的日志，我们需要一个高性能且安全的日志组件，能保证每行日志格式完整性，我们设计了一个类 csv 的日志拼接组件，它的代码在这里 <a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a>。</p><p>它是一个可以保证日志各列完整性且高效拼接字段的组件，支持任意列和行分隔符，而且还支持数组字段，可是实现一对多的日志需求，不用记录多个日志，也不用记录多行。它响应一个 <code>[]byte</code> 数据，方便结合其它主键写入数据到日志文件或者网络中。</p><a id="more"></a><h2 id="使用说明"><a href="#使用说明" class="headerlink" title="使用说明"></a>使用说明</h2><h3 id="API-列表"><a href="#API-列表" class="headerlink" title="API 列表"></a>API 列表</h3><ul><li><p><code>NewRecord(len int) Record</code> 创建长度固定的日志记录</p></li><li><p><code>NewRecordPool(len int) *sync.Pool</code> 创建长度固定的日志记录缓存池</p></li><li><p><code>ToBytes(sep, newline string) []byte</code> 使用 sep 连接 Record，并在末尾添加 newline 换行符</p></li><li><p><code>ArrayJoin(sep string) string</code> 使用 sep 连接 Record，其结果作为数组字段的值</p></li><li><p><code>ArrayFieldJoin(fieldSep, arraySep string) string</code> 使用 fieldSep 连接 Record，其结果作为一个数组的单元</p></li><li><p><code>Clean()</code> 清空 Record 中的所有元素，如果使用 sync.Pool 在放回 Pool 之前应该清空 Record，避免内存泄漏</p></li><li><p><code>UnsafeToBytes(sep, newline string) []byte</code> 使用 sep 连接 Record，并在末尾添加 newline 换行符， 使用原地替换会破坏日志字段引用的字符串</p></li><li><p><code>UnsafeArrayFieldJoin(fieldSep, arraySep string) string</code> 使用 fieldSep 连接 Record，其结果作为一个数组的单元， 使用原地替换会破坏日志字段引用的字符串</p></li></ul><p>底层使用 <code>type Record []string</code> 字符串切片作为一行或者一个数组字段，在使用时它应该是定长的，因为数据日志往往是格式化的，每列都有自己含义，使用 <code>NewRecord(len int) Record</code> 或者 <code>NewRecordPool(len int) *sync.Pool</code> 创建组件，我建议每个日志使用 <code>NewRecordPool</code> 在程序初始化时创建一个缓存池，程序运行时从缓存次获取 <code>Record</code> 将会更加高效，但是每次放回 <code>Pool</code> 时需要调用 <code>Clean</code> 清空 <code>Record</code> 避免引用字符串无法被回收，而导致内存泄漏。</p><h3 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h3><p>我们需要保证日志每列数据的含义一至，我们创建了定长的 <code>Record</code>，但是如何保证每列数据一致性，利用go 的常量枚举可以很好的保证，例如我们定义日志列常量：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> (</span><br><span class="line">LogVersion = <span class="string">&quot;v1.0.0&quot;</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">LogVer = <span class="literal">iota</span></span><br><span class="line">LogTime</span><br><span class="line">LogUid</span><br><span class="line">LogUserName</span><br><span class="line">LogFriends</span><br><span class="line"></span><br><span class="line">LogFieldNumber</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p><code>LogFieldNumber</code> 就是日志的列数量，也就是 <code>Record</code> 的长度，之后使用 <code>NewRecordPool</code> 创建缓存池，然后使用常量名称作为下标记录日志，这样就不用担心因为检查或者疏乎导致日志列错乱的问题了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> w bytes.Buffer <span class="comment">// 一个日志写组件</span></span><br><span class="line"><span class="keyword">var</span> pool = datalog.NewRecordPool(LogFieldNumber) <span class="comment">// 创建一个缓存池</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  r := pool.Get().(datalog.Record)</span><br><span class="line">  r[LogVer] = LogVersion</span><br><span class="line">  r[LogTime] = time.Now().Format(<span class="string">&quot;2006-01-02 15:04:05&quot;</span>)</span><br><span class="line">  <span class="comment">// 检查用户数据是否存在</span></span><br><span class="line">  <span class="comment">//if user ！=nil&#123;</span></span><br><span class="line">    r[LogUid] = <span class="string">&quot;Uid&quot;</span></span><br><span class="line">    r[LogUserName] = <span class="string">&quot;UserNmae&quot;</span></span><br><span class="line">  <span class="comment">//&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 拼接一行日志数据</span></span><br><span class="line">  data := r.Join(datalog.FieldSep, datalog.NewLine)</span><br><span class="line">  r.Clean() <span class="comment">// 清空 Record</span></span><br><span class="line">  pool.Put(r) <span class="comment">// 放回到缓存池</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 写入到日志中</span></span><br><span class="line">  <span class="keyword">if</span> _, err := w.Write(data); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="built_in">panic</span>(err)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 打印出日志数据</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;&#x27;&quot;</span> + w.String() + <span class="string">&quot;&#x27;&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上程序运行会输出：</p><blockquote><p>因为分隔符是不可见字符，下面使用,代替字段分隔符，使用;\n代替换行符， 使用/代替数组字段分隔符，是-代替数组分隔符。</p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#x27;v1.0.0,2019-07-18,11:39:09,Uid,UserNmae,;\n&#x27;</span><br></pre></td></tr></table></figure><p>即使我们没有记录 <code>LogFriends</code> 列的数据，但是在日志中它仍然有一个占位符，如果 <code>user</code> 是 <code>nil</code>，<code>LogUid</code> 和 <code>LogUserName</code> 不需要特殊处理，也不需要写入数据，它依然占据自己的位置，不用担心日志因此而错乱。</p><p>使用 pool 可以很好的利用内存，不会带来过多的内存分配，而且 Record 的每个字段值都是字符串，简单的赋值并不会带来太大的开销，它会指向字符串本身的数据，不会有额外的内存分配，详细参见<a href="https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html">string 优化误区及建议</a>。<br>使用 <code>Record.Join</code> 可以高效的连接一行日志记录，便于我们快速的写入的日志文件中，后面<a href="#%E8%AE%BE%E8%AE%A1%E8%AE%B2%E8%A7%A3">设计讲解</a>部分会详细介绍 <code>Join</code> 的设计。</p><h3 id="包含数组的日志"><a href="#包含数组的日志" class="headerlink" title="包含数组的日志"></a>包含数组的日志</h3><p>有时候也并非都是记录一些单一的值，比如上面 LogFriends 会记录当前记录相关的朋友信息，这可能是一组数据，<a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 也提供了一些简单的辅助函数，可以结合下面的实例实现：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义 LogFriends 数组各列的数据</span></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">LogFriendUid = <span class="literal">iota</span></span><br><span class="line">LogFriendUserName</span><br><span class="line"></span><br><span class="line">LogFriendFieldNumber</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> w bytes.Buffer <span class="comment">// 一个日志写组件</span></span><br><span class="line"><span class="keyword">var</span> pool = datalog.NewRecordPool(LogFieldNumber) <span class="comment">// 每行日志的 pool</span></span><br><span class="line"><span class="keyword">var</span> frPool = datalog.NewRecordPool(LogFriendFieldNumber) <span class="comment">// LogFriends 数组字段的 pool</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="comment">// 程序运行时</span></span><br><span class="line">  r := pool.Get().(datalog.Record)</span><br><span class="line">  r[LogVer] = LogVersion</span><br><span class="line">  r[LogTime] = time.Now().Format(<span class="string">&quot;2006-01-02 15:04:05&quot;</span>)</span><br><span class="line">  <span class="comment">// 检查用户数据是否存在</span></span><br><span class="line">  <span class="comment">//if user ！=nil&#123;</span></span><br><span class="line">    r[LogUid] = <span class="string">&quot;Uid&quot;</span></span><br><span class="line">    r[LogUserName] = <span class="string">&quot;UserNmae&quot;</span></span><br><span class="line">  <span class="comment">//&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 拼接一个数组字段，其长度是不固定的</span></span><br><span class="line">  r[LogFriends] = GetLogFriends(rand.Intn(<span class="number">3</span>))</span><br><span class="line">  <span class="comment">// 拼接一行日志数据</span></span><br><span class="line">  data := r.Join(datalog.FieldSep, datalog.NewLine)</span><br><span class="line">  r.Clean() <span class="comment">// 清空 Record</span></span><br><span class="line">  pool.Put(r) <span class="comment">// 放回到缓存池</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 写入到日志中</span></span><br><span class="line">  <span class="keyword">if</span> _, err := w.Write(data); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="built_in">panic</span>(err)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 打印出日志数据</span></span><br><span class="line">  fmt.Println(<span class="string">&quot;&#x27;&quot;</span> + w.String() + <span class="string">&quot;&#x27;&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义一个函数来拼接 LogFriends </span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetLogFriends</span><span class="params">(friendNum <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">  <span class="comment">// 根据数组长度创建一个 Record，数组的个数往往是不固定的，它整体作为一行日志的一个字段，所以并不会破坏数据</span></span><br><span class="line">fs := datalog.NewRecord(friendNum) </span><br><span class="line"> <span class="comment">// 这里只需要中 pool 中获取一个实例，它可以反复复用</span></span><br><span class="line">fr := frPool.Get().(datalog.Record)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; friendNum; i++ &#123;</span><br><span class="line">    <span class="comment">// fr.Clean() 如果不是每个字段都赋值，应该在使用前或者使用后清空它们便于后面复用</span></span><br><span class="line">fr[LogFriendUid] = <span class="string">&quot;FUid&quot;</span></span><br><span class="line">fr[LogFriendUserName] = <span class="string">&quot;FUserName&quot;</span></span><br><span class="line">    </span><br><span class="line">     <span class="comment">// 连接一个数组中各个字段，作为一个数组单元</span></span><br><span class="line">fs[i] = fr.ArrayFieldJoin(datalog.ArrayFieldSep, datalog.ArraySep)</span><br><span class="line">&#125;</span><br><span class="line">fr.Clean() <span class="comment">// 清空 Record</span></span><br><span class="line">frPool.Put(fr)  <span class="comment">// 放回到缓存池</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 连接数组的各个单元，返回一个字符串作为一行日志的一列</span></span><br><span class="line"><span class="keyword">return</span> fs.ArrayJoin(datalog.ArraySep)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上程序运行会输出：</p><blockquote><p>因为分隔符是不可见字符，下面使用,代替字段分隔符，使用;\n代替换行符， 使用/代替数组字段分隔符，是-代替数组分隔符。</p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#x27;v1.0.0,2019-07-18,11:39:09,Uid,UserNmae,FUid/FUserName-FUid/FUserName;\n&#x27;</span><br></pre></td></tr></table></figure><p>这样在解析时可以把某一字段当做数组解析，这极大的极大的提高了数据日志的灵活性，<br>但是并不建议使用过多的层级，数据日志应当清晰简洁，但是有些特殊场景可以使用一层嵌套。</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><p>使用 <code>ToBytes</code> 和 <code>ArrayFieldJoin</code> 时会把数据字段中的连接字符串替换一个空字符串，所以在 <a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 里面定义了4个分隔符，它们都是不可见字符，极少会出现在数据中，但是我们还需要替换数据中的这些连接字符，避免破坏日志结构。</p><p>虽然组件支持各种连接符，但是为了避免数据被破坏，我们应该选择一些不可见且少见的单字节字符作为分隔符。换行符比较特殊，因为大多数日志读取组件都是用 <code>\n</code> 作为行分隔符，如果数据中极少出现 <code>\n</code> 那就可以使用 <code>\n</code>， <a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 中定义 <code>\x03\n</code> 作为换行符，它兼容一般的日志读取组件，只需要我们做少量的工作就可以正确的解析日志了。</p><p><code>UnsafeToBytes</code> 和 <code>UnsafeArrayFieldJoin</code> 性能会更好，和它们的名字一样，他们并不安全，因为它们使用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#Replace">exbytes.Replace</a> 做原地替换分隔符，这会破坏数据所指向的原始字符串。除非我们日志数据中会出现极多的分隔符需要替换，否者并不建议使用它们，因为它们只在替换时提升性能。</p><p>我在服务中大量使用 <code>UnsafeToBytes</code> 和 <code>UnsafeArrayFieldJoin</code> ，我总是在一个请求结束时记录日志，我确保所有相关的数据不会再使用，所以不用担心原地替换导致其它数据被无感知改变的问题，这也许是一个很好的实践，但是我仍然不推荐使用它们。</p><h2 id="设计讲解"><a href="#设计讲解" class="headerlink" title="设计讲解"></a>设计讲解</h2><p><a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 并没有提供太多的约束很功能，它仅仅包含一种实践和一组辅助工具，在使用它之前，我们需要了解这些实践。</p><p>它帮我们创建一个定长的日志行或者一个<code>sync.Pool</code>，我们需要结合常量枚举记录数据，它帮我们把各列数据连接成记录日志需要的数据格式。</p><p>它所提供的辅助方法都经过实际项目的考验，考量诸多细节，以高性能为核心目标所设计，使用它可以极大的降低相关组件的开发成本，接下来这节将分析它的各个部分。</p><p>我认为值得说道的是它提供的一个 <code>Join</code> 方法，相对于 <code>strings.Join</code> 可以节省两次的内存分配，现从它开始分析。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Join 使用 sep 连接 Record， 并在末尾追加 suffix</span></span><br><span class="line"><span class="comment">// 这个类似 strings.Join 方法，但是避免了连接后追加后缀（往往是换行符）导致的内存分配</span></span><br><span class="line"><span class="comment">// 这个方法直接返回需要的 []byte 类型， 可以减少类型转换，降低内存分配导致的性能问题</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">Join</span><span class="params">(sep, suffix <span class="keyword">string</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(l) == <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">byte</span>(suffix)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">n := <span class="built_in">len</span>(sep) * (<span class="built_in">len</span>(l) - <span class="number">1</span>)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(l); i++ &#123;</span><br><span class="line">n += <span class="built_in">len</span>(l[i])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">n += <span class="built_in">len</span>(suffix)</span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, n)</span><br><span class="line">bp := <span class="built_in">copy</span>(b, l[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="built_in">len</span>(l); i++ &#123;</span><br><span class="line">bp += <span class="built_in">copy</span>(b[bp:], sep)</span><br><span class="line">bp += <span class="built_in">copy</span>(b[bp:], l[i])</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">copy</span>(b[bp:], suffix)</span><br><span class="line"><span class="keyword">return</span> b</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>日志组件往往输入的参数是 <code>[]byte</code> 类型，所以它直接返回一个 <code>[]byte</code> ，而不像 <code>strings.Join</code> 响应一个字符串，在末尾是需要对内部的 <code>buf</code> 进行类型转换，导致额外的内存开销。我们每行日志不仅需要使用分隔符连接各列，还需要一个行分隔符作为结尾，它提供一个后缀 <code>suffix</code>，不用我们之后在 <code>Join</code> 结果后再次拼接行分隔符，这样也能减少一个额外的内存分配。</p><p>这恰恰是 <a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a>  设计的精髓，它并没有大量使用标准库的方法，而是设计更符合该场景的方法，以此来获得更高的性能和更好的使用体验。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ToBytes 使用 sep 连接 Record，并在末尾添加 newline 换行符</span></span><br><span class="line"><span class="comment">// 注意：这个方法会替换 sep 与 newline 为空字符串</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">ToBytes</span><span class="params">(sep, newline <span class="keyword">string</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line">   <span class="keyword">for</span> i := <span class="built_in">len</span>(l) - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i-- &#123;</span><br><span class="line">      <span class="comment">// 提前检查是否包含特殊字符，以便跳过字符串替换</span></span><br><span class="line">      <span class="keyword">if</span> strings.Index(l[i], sep) &lt; <span class="number">0</span> &amp;&amp; strings.Index(l[i], newline) &lt; <span class="number">0</span> &#123;</span><br><span class="line">         <span class="keyword">continue</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      b := []<span class="keyword">byte</span>(l[i]) <span class="comment">// 这会重新分配内存，避免原地替换导致引用字符串被修改</span></span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(sep), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(newline), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      l[i] = exbytes.ToString(b)</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">return</span> l.Join(sep, newline)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ToBytes</code> 作为很重要的交互函数，也是该组件使用频率最高的函数，它在连接各个字段之前替换每个字段中的字段和行分隔符，这里提前做了一个检查字段中是否包含分隔符，如果包含使用 <code>[]byte(l[i])</code> 拷贝该列的数据，然后使用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#Replace">exbytes.Replace</a> 提供高性能的原地替换，因为输入数据是拷贝重新分配的，所以不用担心原地替换会影响其它数据。</p><p>之后使用之前介绍的 <code>Join</code> 方法连接各列数据，如果使用 <code>strings.Join</code> 将会是 <code>[]byte(strings.Join([]string(l), sep) + newline)</code> 这其中会增加很多次内存分配，该组件通过巧妙的设计规避这些额外的开销，以提升性能。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UnsafeToBytes 使用 sep 连接 Record，并在末尾添加 newline 换行符</span></span><br><span class="line"><span class="comment">// 注意：这个方法会替换 sep 与 newline 为空字符串，替换采用原地替换，这会导致所有引用字符串被修改</span></span><br><span class="line"><span class="comment">// 必须明白其作用，否者将会导致意想不到的结果。但是这会大幅度减少内存分配，提升程序性能</span></span><br><span class="line"><span class="comment">// 我在项目中大量使用，我总是在请求最后记录日志，这样我不会再访问引用的字符串</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">UnsafeToBytes</span><span class="params">(sep, newline <span class="keyword">string</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line">   <span class="keyword">for</span> i := <span class="built_in">len</span>(l) - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i-- &#123;</span><br><span class="line">      b := exstrings.UnsafeToBytes(l[i])</span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(sep), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(newline), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      l[i] = exbytes.ToString(b)</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">return</span> l.Join(sep, newline)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>UnsafeToBytes</code> 和 <code>ToBytes</code> 相似只是没有分割符检查，因为<a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#Replace">exbytes.Replace</a> 中已经包含了检查，而且直接使用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings#UnsafeToBytes">exstrings.UnsafeToBytes</a> 把字符串转成  <code>[]byte</code>  这不会发生数据拷贝，非常的高效，但是它不支持字面量字符串，不过我相信日志中的数据均来自运行时分配，如果不幸包含字面量字符串，也不用太过担心，只要使用一个特殊的字符作为分隔符，往往我们编程字面量字符串并不会包含这些字符，执行 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#Replace">exbytes.Replace</a> 没有发生替换也是安全的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Clean 清空 Record 中的所有元素，如果使用 sync.Pool 在放回 Pool 之前应该清空 Record，避免内存泄漏</span></span><br><span class="line"><span class="comment">// 该方法没有太多的开销，可以放心的使用，只是为 Record 中的字段赋值为空字符串，空字符串会在编译时处理，没有额外的内存分配</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">Clean</span><span class="params">()</span></span> &#123;</span><br><span class="line">   <span class="keyword">for</span> i := <span class="built_in">len</span>(l) - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i-- &#123;</span><br><span class="line">      l[i] = <span class="string">&quot;&quot;</span></span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Clean</code> 方法更简单，它只是把各个列的数据替换为空字符串，空字符串做为一个特殊的字符，会在编译时处理，并不会有额外的开销，它们都指向同一块内存。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ArrayJoin 使用 sep 连接 Record，其结果作为数组字段的值</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">ArrayJoin</span><span class="params">(sep <span class="keyword">string</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">   <span class="keyword">return</span> exstrings.Join(l, sep)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ArrayFieldJoin 使用 fieldSep 连接 Record，其结果作为一个数组的单元</span></span><br><span class="line"><span class="comment">// 注意：这个方法会替换 fieldSep 与 arraySep 为空字符串，替换采用原地替换</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l Record)</span> <span class="title">ArrayFieldJoin</span><span class="params">(fieldSep, arraySep <span class="keyword">string</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">   <span class="keyword">for</span> i := <span class="built_in">len</span>(l) - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i-- &#123;</span><br><span class="line">      <span class="comment">// 提前检查是否包含特殊字符，以便跳过字符串替换</span></span><br><span class="line">      <span class="keyword">if</span> strings.Index(l[i], fieldSep) &lt; <span class="number">0</span> &amp;&amp; strings.Index(l[i], arraySep) &lt; <span class="number">0</span> &#123;</span><br><span class="line">         <span class="keyword">continue</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      b := []<span class="keyword">byte</span>(l[i]) <span class="comment">// 这会重新分配内存，避免原地替换导致引用字符串被修改</span></span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(fieldSep), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      b = exbytes.Replace(b, exstrings.UnsafeToBytes(arraySep), []<span class="keyword">byte</span>&#123;<span class="string">&#x27; &#x27;</span>&#125;, <span class="number">-1</span>)</span><br><span class="line">      l[i] = exbytes.ToString(b)</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">return</span> exstrings.Join(l, fieldSep)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ArrayFieldJoin</code> 在连接各个字符串时会直接替换数组单元分隔符，之后直接使用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings#Join">exstrings.Join</a> 进行连接字符串，<a href="%5Bexstrings.Join%5D(https://godoc.org/github.com/thinkeridea/go-extend/exstrings#Join)">exstrings.Join</a> 相对 <code>strings.Join</code> 的一个改进函数，因为它只有一次内存分配，较 <code>strings.Join</code> 节省一次，有兴趣可以去看它的源码实现。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 提供了一种实践以及一些辅助工具，可以帮助我们快速的记录数据日志，更关心数据本身。具体程序性能可以交给 <a href="https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go">datalog</a> 来实现，它保证程序的性能。</p><p>后期我会计划提供一个高效的日志读取组件，以便于读取解析数据日志，它较与一般文件读取会更加高效且便捷，有针对性的优化日志解析效率，敬请关注吧。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;我们业务每天需要记录大量的日志数据，且这些数据十分重要，它们是公司收入结算的主要依据，也是数据分析部门主要得数据源，针对这么重要的日志，且高频率的日志，我们需要一个高性能且安全的日志组件，能保证每行日志格式完整性，我们设计了一个类 csv 的日志拼接组件，它的代码在这里 &lt;a href=&quot;https://github.com/thinkeridea/go-extend/blob/master/datalog/record.go&quot;&gt;datalog&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;它是一个可以保证日志各列完整性且高效拼接字段的组件，支持任意列和行分隔符，而且还支持数组字段，可是实现一对多的日志需求，不用记录多个日志，也不用记录多行。它响应一个 &lt;code&gt;[]byte&lt;/code&gt; 数据，方便结合其它主键写入数据到日志文件或者网络中。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="pool" scheme="https://blog.thinkeridea.com/tags/pool/"/>
    
    <category term="日志" scheme="https://blog.thinkeridea.com/tags/%E6%97%A5%E5%BF%97/"/>
    
    <category term="csv" scheme="https://blog.thinkeridea.com/tags/csv/"/>
    
    <category term="join" scheme="https://blog.thinkeridea.com/tags/join/"/>
    
  </entry>
  
  <entry>
    <title>【Go】使用压缩文件优化io (二)</title>
    <link href="https://blog.thinkeridea.com/201907/go/compress_file_io_optimization2.html"/>
    <id>https://blog.thinkeridea.com/201907/go/compress_file_io_optimization2.html</id>
    <published>2019-07-07T09:32:51.000Z</published>
    <updated>2019-07-08T01:52:16.134Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html">上一篇文章《使用压缩文件优化io (一)》</a>中记录了日志备份 io 优化方案，使用文件流数据压缩方案优化 io 性能，效果十分显著。这篇文章记录数据分析前置清洗、格式化数据的 io 优化方案，我们有一台专用的日志前置处理服务器，所有业务日志通过这台机器从 OSS 拉取回来清洗、格式化，最后进入到数据仓储中便于后续的分析。</p><p>随着业务扩展这台服务器压力越来越大，高峰时数据延迟越来越厉害，早期也是使用 Python 脚本 + awk 以及一些 shell 命令完成相关工作，在数据集不是很大的时候这种方案很好，效率也很高，随着数据集变大，发现服务器负载很高，经过分析是还是 io 阻塞，依旧采用对数据流进行处理的方案优化io，以下记录优化的过程。</p><a id="more"></a><h2 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h2><blockquote><p>服务器配置：4 核 8G； 磁盘：1T</p></blockquote><p>分析前置服务会根据业务不同分为十分钟、一小时两个阶段拉取分析日志，每隔一个阶段会去 OSS 拉取日志回到服务器进行处理，处理过程因 io 阻塞，导致 CPU 和 load 异常高，且处理效率严重下降，这次优化主要就是降低 io 阻塞，提升 CPU 利用率 (处理业务逻辑而不是等待 io) 和处理效率。</p><p>后文中会详细描述优化前后的方案，并用 go 编写测试，使用一台 2 核4G的服务器进行测试，测试数据集大小为：</p><ul><li>文件数量：432个</li><li>压缩文件：17G</li><li>解压后文件：63G </li><li>压缩方案：lzo</li><li>Goroutine 数量：20</li></ul><h2 id="优化前"><a href="#优化前" class="headerlink" title="优化前"></a>优化前</h2><p>优化前日志处理流程：</p><ul><li>获取待处理文件列表</li><li>拉取 OSS 日志到本地磁盘 (压缩文件)</li><li>解压缩日志文件</li><li>读取日志数据</li><li>业务处理……</li><li>导入到数据仓储中</li></ul><p>导致 io 阻塞的部分主要是： 拉取 OSS 日志、解压缩日志文件及读取日志数据，优化也主要从这三块着手。</p><p>这里有一段公共的日志读取方法，该方法接收一个 <code>io.Reader</code>， 并按行读取日志，并简单切分日志字段，并没有实质的处理日志数据，后面的优化方案也将使用这个方法读取日志。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bufio&quot;</span></span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exbytes&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Read</span><span class="params">(r io.Reader)</span></span> &#123;</span><br><span class="line">rawBuffer := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">512</span>)</span><br><span class="line">buf := bufio.NewReader(r)</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">line, ok, err := readLine(buf, rawBuffer)</span><br><span class="line"><span class="keyword">if</span> err == io.EOF &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ok &#123;</span><br><span class="line">rawBuffer = line</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c := bytes.Count(line, []<span class="keyword">byte</span>&#123;<span class="string">&#x27;\x01&#x27;</span>&#125;)</span><br><span class="line"><span class="keyword">if</span> c != <span class="number">65</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;无效的行&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">readLine</span><span class="params">(r *bufio.Reader, rawBuffer []<span class="keyword">byte</span>)</span> <span class="params">([]<span class="keyword">byte</span>, <span class="keyword">bool</span>, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> ok <span class="keyword">bool</span></span><br><span class="line">line, err := r.ReadSlice(<span class="string">&#x27;\n&#x27;</span>)</span><br><span class="line"><span class="keyword">if</span> (err == bufio.ErrBufferFull || <span class="built_in">len</span>(line) &lt; <span class="number">3</span> || exbytes.ToString(line[<span class="built_in">len</span>(line)<span class="number">-3</span>:]) != <span class="string">&quot;\r\r\n&quot;</span>) &amp;&amp; err != io.EOF &#123;</span><br><span class="line">rawBuffer = <span class="built_in">append</span>(rawBuffer[:<span class="number">0</span>], line...)</span><br><span class="line"><span class="keyword">for</span> (err == bufio.ErrBufferFull || <span class="built_in">len</span>(line) &lt; <span class="number">3</span> || exbytes.ToString(line[<span class="built_in">len</span>(line)<span class="number">-3</span>:]) != <span class="string">&quot;\r\r\n&quot;</span>) &amp;&amp; err != io.EOF &#123;</span><br><span class="line">line, err = r.ReadSlice(<span class="string">&#x27;\n&#x27;</span>)</span><br><span class="line">rawBuffer = <span class="built_in">append</span>(rawBuffer, line...)</span><br><span class="line">&#125;</span><br><span class="line">line = rawBuffer</span><br><span class="line">ok = <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(line) &gt; <span class="number">0</span> &amp;&amp; err == io.EOF &#123;</span><br><span class="line">err = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> line, ok, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>日志按 <code>\r\r\n</code> 分隔行，使用 <code>\x01</code> 切分字段，读取方法使用 <code>bufio.ReadSlice</code> 方法，避免内存分配，且当 <code>bufio</code> 缓冲区满之后使用 <code>rwaBuffer</code> 作为本地可扩展缓冲，每次扩展之后会保留最大的扩展空间，因为业务日志每行大小差不多，这样可以极大的减少内存分配，效率是 <code>bufio.ReadLine</code> 方法的好几倍。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;os&quot;</span></span><br><span class="line"><span class="string">&quot;os/exec&quot;</span></span><br><span class="line"><span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;.../pkg/aliyun_oss&quot;</span> <span class="comment">// 虚假的包</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> oss *aliyun_oss.AliyunOSS <span class="comment">// 对接阿里云 OSS，需要自行封装</span></span><br><span class="line">files := list()               <span class="comment">// 这是一个虚构的方法，用来获取待处理的文件列表</span></span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;待处理文件数量：%d\n&quot;</span>, <span class="built_in">len</span>(files))</span><br><span class="line"></span><br><span class="line">start := time.Now()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">(t time.Time)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;共耗时：%0.6f\n&quot;</span>, time.Now().Sub(t).Seconds())</span><br><span class="line">&#125;(start)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 下载日志文件</span></span><br><span class="line">n := <span class="number">20</span></span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">f, ok := &lt;-c</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, err := os.Stat(f); err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> !os.IsNotExist(err) &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">dir := filepath.Dir(f)</span><br><span class="line">err := os.MkdirAll(dir, <span class="number">0755</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">err = oss.GetObjectToFile(f, f)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;下载文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line"></span><br><span class="line"><span class="comment">// 解压日志文件</span></span><br><span class="line">start = time.Now()</span><br><span class="line">shell := exec.Command(<span class="string">&quot;/bin/bash&quot;</span>, <span class="string">&quot;-c&quot;</span>, <span class="string">&quot;lzop -df logs/*/*/*/*/*/*.lzo&quot;</span>)</span><br><span class="line">err := shell.Run()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;解压文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line"></span><br><span class="line"><span class="comment">// 读取日志文件</span></span><br><span class="line">start = time.Now()</span><br><span class="line">c = <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">file, ok := &lt;-c</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">f, err := os.Open(file)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Read(f)</span><br><span class="line">f.Close()</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- strings.TrimRight(f, <span class="string">&quot;.lzo&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;读取文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行程序输出如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">待处理文件数量：<span class="number">432</span></span><br><span class="line">下载文件耗时：<span class="number">303.562865</span></span><br><span class="line">解压文件耗时：<span class="number">611.236232</span></span><br><span class="line">读取文件耗时：<span class="number">460.371245</span></span><br><span class="line">共耗时：<span class="number">1375.187261</span></span><br></pre></td></tr></table></figure><p>通过 <code>iostat -m -x 5 10000</code> 分析各个阶段结果如下：</p><ul><li>下载时：</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">   7.85    0.00   16.44   11.24    0.00   64.48</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00   80.40    7.80     8.98     0.04   209.36     0.40    4.57    4.64    3.77   0.50   4.44</span><br><span class="line">vdb               1.40   761.20  247.60  264.00    14.70    60.92   302.72     9.17   17.92   10.36   25.00   0.52  26.52</span><br></pre></td></tr></table></figure><ul><li>解压时：</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">   8.54    0.00    8.33   68.39    0.00   14.74</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     1.20    3.40   11.80     0.01     0.05     8.95     0.30   20.03    0.41   25.68   0.55   0.84</span><br><span class="line">vdb               0.00 22037.80  107.80  243.20    26.45   107.01   778.71    83.52  236.68   74.31  308.65   2.52  88.54</span><br></pre></td></tr></table></figure><ul><li>读取时：</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">   2.74    0.00    5.07   92.19    0.00    0.00</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     2.40    3.80   23.60     0.01     0.14    11.85     0.12    4.48    1.95    4.89   0.33   0.90</span><br><span class="line">vdb               1.80     4.60  347.20    6.20   139.97     0.08   811.60   126.62  358.04  360.79  203.48   2.83 100.00</span><br></pre></td></tr></table></figure><p>通过 <code>iostat</code> 结果可以看出，在解压和读取日志时 <code>io</code> 阻塞比较严重，且运行时间较长，下载时 <code>io</code> 阻塞也存在，但还可以接受，通过下面两个方案逐渐消除掉 <code>io</code>。</p><h2 id="优化方案一"><a href="#优化方案一" class="headerlink" title="优化方案一"></a>优化方案一</h2><p>优化前的方案反应出在解压和读取日志时 <code>io</code> 阻塞比较严重，那么是否可以通过读取 <code>lzo</code> 压缩文件，以此来消除解压缩日志耗时太大、<code>io</code> 太高的问题呢？并且读取 <code>lzo</code> 压缩文件远比解压后文件小，来降低读取日志耗时太大、<code>io</code> 太高的问题呢？</p><p>优化后日志处理流程：</p><ul><li>获取待处理文件列表</li><li>拉取 OSS 日志到本地磁盘 (压缩文件)</li><li>读取压缩日志数据</li><li>业务处理……</li><li>导入到数据仓储中</li></ul><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;os&quot;</span></span><br><span class="line"><span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;.../pkg/aliyun_oss&quot;</span> <span class="comment">// 虚假的包</span></span><br><span class="line"><span class="string">&quot;github.com/cyberdelia/lzo&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> oss *aliyun_oss.AliyunOSS <span class="comment">// 对接阿里云 OSS，需要自行封装</span></span><br><span class="line">files := list()               <span class="comment">// 这是一个虚构的方法，用来获取待处理的文件列表</span></span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;待处理文件数量：%d\n&quot;</span>, <span class="built_in">len</span>(files))</span><br><span class="line">start := time.Now()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">(t time.Time)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;共耗时：%0.6f\n&quot;</span>, time.Now().Sub(t).Seconds())</span><br><span class="line">&#125;(start)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 下载日志文件</span></span><br><span class="line">n := <span class="number">20</span></span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">f, ok := &lt;-c</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, err := os.Stat(f); err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> !os.IsNotExist(err) &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">dir := filepath.Dir(f)</span><br><span class="line">err := os.MkdirAll(dir, <span class="number">0755</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">err = oss.GetObjectToFile(f, f)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;下载文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line"></span><br><span class="line">start = time.Now()</span><br><span class="line">c = <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">file, ok := &lt;-c</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">f, err := os.Open(file)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">r, err := lzo.NewReader(f)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Read(r)</span><br><span class="line">r.Close()</span><br><span class="line">f.Close()</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;读取文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个方案消除了解压缩日志，并且直接读取压缩日志，使用 <code>github.com/cyberdelia/lzo</code> 包对压缩文件数据流进行边读取边解压，这次不用单独封装新的方法了，直接使用 <code>lzo</code> 包中的接口即可。</p><p>程序运行结果如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">待处理文件数量：432</span><br><span class="line">下载文件耗时：286.146603</span><br><span class="line">读取文件耗时：132.787345</span><br><span class="line">共耗时：418.942862</span><br></pre></td></tr></table></figure><p>这个方案效果非常明显，总耗时从 <code>1375.187261</code> 降低到 <code>418.942862</code> 提升了 3 倍的效率，不仅消除了压缩的时间，还大大缩短了读取文件耗时，成果显著。</p><p>通过 <code>iostat -m -x 5 10000</code> 分析各个阶段结果如下：</p><p>下载时：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">   5.08    0.00   13.24   29.34    0.00   52.33</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     2.80    1.40   11.80     0.01     0.07    12.00     0.02    1.85    1.14    1.93   0.18   0.24</span><br><span class="line">vdb               0.00 17207.60    0.60  212.40     0.00    75.06   721.74    55.81  236.34   84.33  236.77   2.49  53.14</span><br></pre></td></tr></table></figure><p>读取时：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">  80.66    0.00    4.83   14.50    0.00    0.00</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    6.20    0.20     0.06     0.00    20.00     0.01    1.69    1.71    1.00   0.62   0.40</span><br><span class="line">vdb               0.00     6.80  390.40   19.20   118.78     0.23   595.04    74.87  190.55  197.95   40.08   1.85  75.90</span><br></pre></td></tr></table></figure><p>通过 <code>iostat</code> 结果分析，下载时 <code>io</code> 阻塞和优化前波动不是很大，读取时的 <code>io</code> 优化已经非常好了，<code>iowait</code> 从 <code>92.19%</code> 降低到 <code>14.5%</code> ，CPU 更多的任务用来处理解压缩日志，而不是处理 <code>io</code> 阻塞。</p><h2 id="优化方案二"><a href="#优化方案二" class="headerlink" title="优化方案二"></a>优化方案二</h2><p>本来优化到上面的效果已经非常满意了，不过既然开始做优化就不能草草结束了，仔细思考业务场景，需要 本地 <code>lzo</code> 文件？重新处理日志的频率高吗？本地 <code>lzo</code> 日志清理方便吗？</p><p>通过上面的几个问题发现，除非程序出现问题或者数据存储出现故障，否者极少需要重新处理日志，一年里面这种情况也是极少的，甚至不会发生。</p><p>那么思考一下，不下载日志，直接读取网络数据流，实现边下边解压边读取，这样岂不是没有 <code>io</code> 了吗?</p><p>优化后日志处理流程：</p><ul><li>获取待处理文件列表</li><li>拉取 OSS 日志，在内存中解压并读取分析日志</li><li>业务处理……</li><li>导入到数据仓储中</li></ul><p>具体实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;.../pkg/aliyun_oss&quot;</span> <span class="comment">// 虚假的包</span></span><br><span class="line"><span class="string">&quot;github.com/cyberdelia/lzo&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> oss *aliyun_oss.AliyunOSS <span class="comment">// 对接阿里云 OSS，需要自行封装</span></span><br><span class="line">files := list()               <span class="comment">// 这是一个虚构的方法，用来获取待处理的文件列表</span></span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;待处理文件数量：%d\n&quot;</span>, <span class="built_in">len</span>(files))</span><br><span class="line"></span><br><span class="line">start := time.Now()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">(t time.Time)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;共耗时：%0.6f\n&quot;</span>, time.Now().Sub(t).Seconds())</span><br><span class="line">&#125;(start)</span><br><span class="line"></span><br><span class="line">n := <span class="number">20</span></span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">f, ok := &lt;-c</span><br><span class="line"><span class="keyword">if</span> !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">r1, err := oss.GetObject(f)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">r, err := lzo.NewReader(r1)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Read(r)</span><br><span class="line">r.Close()</span><br><span class="line">r1.Close()</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;读取文件耗时：%0.6f\n&quot;</span>, time.Now().Sub(start).Seconds())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>优化后只有一个流程了，代码简洁了不少，看看效率如何吧！</p><p>程序运行结果如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">待处理文件数量：432</span><br><span class="line">读取文件耗时：285.993661</span><br><span class="line">共耗时：285.993717</span><br></pre></td></tr></table></figure><p>天啊发生了什么，我使劲擦了擦眼睛，太不可思议了，居然只消耗了下载日志的耗时，较上一个方案总耗时从 <code>418.942862</code> 降低到 <code>285.993717</code>，提升了近 2 倍的效率，让我们看看上个方案下载文件耗时 <code>286.146603</code> ，而新方案总耗时是 <code>285.993717</code> 居然只用了上个优化版本的下载时间，究竟发生了什么？</p><p>通过 <code>iostat -m -x 5 10000</code> 分析结果如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">  43.73    0.00    9.64    0.31    0.00   46.32</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rMB/s    wMB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     1.20    4.40    3.80     0.02     0.03    10.93     0.01    1.49    0.59    2.53   0.71   0.58</span><br><span class="line">vdb               0.00     6.80    0.00   24.60     0.00     0.27    22.83     0.02    0.84    0.00    0.84   0.28   0.68</span><br></pre></td></tr></table></figure><p>通过 <code>iostat</code> 结果分析，在程序运行期间没有任何 <code>io</code> 开销，CPU 居然还有一半的空闲，前面两个版本 CPU 是没有空闲的啊，由此看来之前 CPU 更多的消耗在 <code>io</code> 阻塞上了，并没有用来处理业务逻辑。</p><p>由此来看也就不足为奇了，为啥优化后只需要下载日志的时间就能处理完所有日志了，没有了 <code>io</code> 阻塞，CPU 更多了用来处理业务，把之前下载时写文件 <code>io</code> 的耗时，用来解压缩数据，读取数据，且还有更多的空闲，跑出这样的结果也就很正常了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>从优化前耗时 <code>1375.187261</code> 秒到 <code>285.993717</code> 秒，性能提升 80%， 从 <code>iowait</code> <code>92.19%</code> 到 <code>0.31%</code> 提升近 <code>100%</code>，从没有任何 CPU 空闲到有一半空闲，这个过程中有很多值得总结的事情。</p><p><code>io</code> 对性能的影响非常大，对 CPU 占用非常严重，导致 CPU 处理业务逻辑的时间片非常少。从  <code>io</code> 转移到 CPU 对性能提升非常明显。CPU 计算效率十分的高，从 <code>io</code> 密集到密集计算，只要符合业务场景，往往能给我们带来意想不到的效果。</p><p>往往优化业务并不需要十分高大上的技术，只是转变一下思路，不仅代码更少，且程序更简短、好维护、逻辑更清晰。</p><p>一定要结合实际业务场景进行思考，减少理所当然和业务无关的中间环节，往往就可以极大的提升程序效率。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html&quot;&gt;上一篇文章《使用压缩文件优化io (一)》&lt;/a&gt;中记录了日志备份 io 优化方案，使用文件流数据压缩方案优化 io 性能，效果十分显著。这篇文章记录数据分析前置清洗、格式化数据的 io 优化方案，我们有一台专用的日志前置处理服务器，所有业务日志通过这台机器从 OSS 拉取回来清洗、格式化，最后进入到数据仓储中便于后续的分析。&lt;/p&gt;
&lt;p&gt;随着业务扩展这台服务器压力越来越大，高峰时数据延迟越来越厉害，早期也是使用 Python 脚本 + awk 以及一些 shell 命令完成相关工作，在数据集不是很大的时候这种方案很好，效率也很高，随着数据集变大，发现服务器负载很高，经过分析是还是 io 阻塞，依旧采用对数据流进行处理的方案优化io，以下记录优化的过程。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="compress" scheme="https://blog.thinkeridea.com/tags/compress/"/>
    
    <category term="io" scheme="https://blog.thinkeridea.com/tags/io/"/>
    
    <category term="压缩" scheme="https://blog.thinkeridea.com/tags/%E5%8E%8B%E7%BC%A9/"/>
    
  </entry>
  
  <entry>
    <title>【Go】使用压缩文件优化io (一)</title>
    <link href="https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html"/>
    <id>https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html</id>
    <published>2019-06-30T11:02:57.000Z</published>
    <updated>2019-07-07T09:28:21.569Z</updated>
    
    <content type="html"><![CDATA[<p>最近遇到一个日志备份 io 过高的问题，业务日志每十分钟备份一次，本来是用 Python 写一个根据规则扫描备份日志问题不大，但是随着业务越来越多，单机上的日志文件越来越大，文件数量也越来越多，导致每每备份的瞬间 io 阻塞严重， CPU 和 load 异常的高，好在备份速度很快，对业务影响不是很大，这个问题会随着业务增长，越来越明显，这段时间抽空对备份方式做了优化，效果十分显著，整理篇文章记录一下。</p><a id="more"></a><h2 id="背景说明"><a href="#背景说明" class="headerlink" title="背景说明"></a>背景说明</h2><blockquote><p>服务器配置：4 核  8G； 磁盘：500G</p><p>每十分钟需要上传：18 个文件，高峰时期约  10 G 左右</p></blockquote><p>业务日志为了保证可靠性，会先写入磁盘文件，每10分钟切分日志文件，然后在下十分钟第一分时备份日志到 OSS，数据分析服务会从在备份完成后拉取日志进行分析，日志备份需要高效快速，在最短的时间内备份完，一般备份均能在几十秒内完成。</p><p>备份的速度和效率并不是问题，足够的快，但是在备份时 io 阻塞严重导致的 CPU 和 load 异常，成为业务服务的瓶颈，在高峰期业务服务仅消耗一半的系统资源，但是备份时 CPU 经常 100%，且 iowait 可以达到 70 多，空闲资源非常少，这样随着业务扩展，日志备份虽然时间很短，却成为了系统的瓶颈。</p><p>后文中会详细描述优化前后的方案，并用 go 编写测试，使用一台 2 核4G的服务器进行测试，测试数据集大小为：</p><ul><li>文件数：336</li><li>原始文件：96G</li><li>压缩文件：24G</li><li>压缩方案：lzo</li><li>Goroutine 数量：4</li></ul><h2 id="优化前"><a href="#优化前" class="headerlink" title="优化前"></a>优化前</h2><p>优化前日志备份流程：</p><ul><li>根据备份规则扫描需要备份的文件</li><li>使用 <code>lzop</code> 命令压缩日志</li><li>上传压缩后的日志到 OSS</li></ul><p>下面是代码实现，这里不再包含备份文件规则，仅演示压缩上传逻辑部分，程序接受文件列表，并对文件列表压缩上传至 OSS 中。</p><p><code>.../pkg/aliyun_oss</code> 是我自己封装的基于阿里云 OSS 操作的包，这个路径是错误的，仅做演示，想运行下面的代码，OSS 交互这部分需要自己实现。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;os&quot;</span></span><br><span class="line"><span class="string">&quot;os/exec&quot;</span></span><br><span class="line"><span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;.../pkg/aliyun_oss&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> oss *aliyun_oss.AliyunOSS</span><br><span class="line">files := os.Args[<span class="number">1</span>:]</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(files) &lt; <span class="number">1</span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;请输入要上传的文件&quot;</span>)</span><br><span class="line">os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;待备份文件数量：%d\n&quot;</span>, <span class="built_in">len</span>(files))</span><br><span class="line"></span><br><span class="line">startTime := time.Now()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">(startTime time.Time)</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;共耗时：%s\n&quot;</span>, time.Now().Sub(startTime).String())</span><br><span class="line">&#125;(startTime)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">n := <span class="number">4</span></span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 压缩日志</span></span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> file := <span class="keyword">range</span> c &#123;</span><br><span class="line">cmd := exec.Command(<span class="string">&quot;lzop&quot;</span>, file)</span><br><span class="line">cmd.Stderr = &amp;bytes.Buffer&#123;&#125;</span><br><span class="line">err := cmd.Run()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(cmd.Stderr.(*bytes.Buffer).String())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- file</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line">fmt.Printf(<span class="string">&quot;压缩耗时：%s\n&quot;</span>, time.Now().Sub(startTime).String())</span><br><span class="line"></span><br><span class="line"><span class="comment">// 上传压缩日志</span></span><br><span class="line">startTime = time.Now()</span><br><span class="line">c = <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">for</span> file := <span class="keyword">range</span> c &#123;</span><br><span class="line">name := filepath.Base(file)</span><br><span class="line">err := oss.PutObjectFromFile(<span class="string">&quot;tmp/&quot;</span>+name+<span class="string">&quot;.lzo&quot;</span>, file+<span class="string">&quot;.lzo&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- file</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line">fmt.Printf(<span class="string">&quot;上传耗时：%s\n&quot;</span>, time.Now().Sub(startTime).String())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>程序运行时输出：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">待备份文件数量：336</span><br><span class="line">压缩耗时：19m44.125314226s</span><br><span class="line">上传耗时：6m14.929371103s</span><br><span class="line">共耗时：25m59.118002969s</span><br></pre></td></tr></table></figure><p>从运行结果中可以看出压缩文件耗时很久，实际通过 <code>iostat</code> 命令分析也发现，压缩时资源消耗比较高，下面是 <code>iostat -m -x 5 10000</code> 命令采集各个阶段数据。</p><ul><li>程序运行前</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">           2.35    0.00    2.86    0.00    0.00   94.79</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00</span><br><span class="line">vdb               0.00     0.60    0.00    0.60     0.00     4.80    16.00     0.00    0.67    0.00    0.67   0.67   0.04</span><br></pre></td></tr></table></figure><ul><li>压缩日志时</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">          10.84    0.00    6.85   80.88    0.00    1.43</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    0.60    0.00     2.40     0.00     8.00     0.00    0.67    0.67    0.00   0.67   0.04</span><br><span class="line">vdb              14.80  5113.80 1087.60   60.60 78123.20 20697.60   172.13   123.17  106.45  106.26  109.87   0.87 100.00</span><br><span class="line"></span><br><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">          10.06    0.00    7.19   79.06    0.00    3.70</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    1.60    0.00   103.20     0.00   129.00     0.01    3.62    3.62    0.00   0.50   0.08</span><br><span class="line">vdb              14.20  4981.20  992.80   52.60 79682.40 20135.20   190.97   120.34  112.19  110.60  142.17   0.96 100.00</span><br></pre></td></tr></table></figure><ul><li>上传日志时</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">           6.98    0.00    7.81    7.71    0.00   77.50</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00   13.40    0.00   242.40     0.00    36.18     0.02    1.63    1.63    0.00   0.19   0.26</span><br><span class="line">vdb               0.40     2.40  269.60    1.20 67184.80    14.40   496.30     4.58   15.70   15.77    0.33   1.39  37.74</span><br><span class="line"></span><br><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">           7.06    0.00    8.00    4.57    0.00   80.37</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    0.60    0.00    75.20     0.00   250.67     0.00    2.67    2.67    0.00   2.00   0.12</span><br><span class="line">vdb               0.20     0.00  344.80    0.00 65398.40     0.00   379.34     5.66   16.42   16.42    0.00   1.27  43.66</span><br></pre></td></tr></table></figure><p>从 <code>iostat</code> 的结果中发现，压缩时程序 <code>r_await</code> 和 <code>w_await</code> 都到了一百多，且 <code>iowait</code> 高达 <code>80.88%</code>，几乎耗尽了所有的 CPU，上传时 <code>iowait</code> 是可以接受的，因为只是单纯的读取压缩文件，且压缩文件也很小。</p><h2 id="分析问题"><a href="#分析问题" class="headerlink" title="分析问题"></a>分析问题</h2><p>上述结果中发现程序主要运行消耗在压缩日志，那优化也着重日志压缩的逻辑上。</p><p>压缩时日志会先压缩成 <code>lzo</code> 文件，然后再上传 <code>lzo</code> 文件到阿里云 OSS 上，这中间发生了几个过程：</p><ul><li>读取原始日志文件</li><li>压缩数据</li><li>写入 <code>lzo</code> 文件</li><li>读取 <code>lzo</code> 文件</li><li><code>http</code> 发送读取的内容</li></ul><p>压缩时 <code>r_await</code> 和 <code>w_await</code> 都很高，主要发生在读取原始日志文件，写入 <code>lzo</code> 文件， 怎么优化呢？</p><p>先想一下原始需求，读取原始文件 -&gt; 上传数据。但是直接上传原始文件，文件比较大，网络传输慢，而且存储费用也比较高，怎么办呢？</p><p>这个时候我们期望可以上传的是压缩文件，所以就有了优化前的逻辑，这里面产生了一个中间过程，即使用 <code>lzop</code> 命令压缩文件，而且产生了一个中间文件 <code>lzo</code> 文件。</p><p>读取原始文件和上传数据是必须的，那么可以优化的就是压缩的流程了，所以 <code>r_await</code> 是没有办法优化的，那么只能优化 <code>w_await</code>，<code>w_await</code> 是怎么产生的呢，恰恰是写入<code>lzo</code> 时产生的，可以不要 <code>lzo</code> 文件吗？这个文件有什么作用？</p><p>如果我们压缩文件数据流，在 读取原始文件 -&gt; 上传数据 流程中对上传的数据流进行实时压缩，把压缩的内容给上传了，实现边读边压缩，对数据流进行处理，像是一个中间件，这样就不用写 <code>lzo</code> 文件了，那么 <code>w_await</code> 就被完全优化没了。</p><p><code>lzo</code> 文件有什么作用？我想只有在上传失败之后可以节省一次文件压缩的消耗。上传失败的次数多吗？我用阿里云 OSS 好几年了，除了一次内网故障，再也没有遇到过上传失败的文件，我想是不需要这个文件的，而且生成 <code>lzo</code> 文件还需要占用磁盘空间，定时清理等等，增加了资源消耗和维护成本。</p><h2 id="优化后"><a href="#优化后" class="headerlink" title="优化后"></a>优化后</h2><p>根据之前的分析看一下优化之后备份文件需要哪些过程：</p><ul><li>读取原始日志</li><li>在内存中压缩数据流</li><li>http 发送压缩后的内容</li></ul><p>这个流程节省了两个步骤，写入 <code>lzo</code> 文件和 读取 <code>lzo</code> 文件，不仅没有 <code>w_await</code>，就连 <code>r_await</code> 也得到了小幅度的优化。</p><p>优化方案确定了，可是怎么实现 <code>lzo</code> 对文件流进行压缩呢，去 <code>Github</code> 上找一下看看有没有 <code>lzo</code> 的压缩算法库，发现 <code>github.com/cyberdelia/lzo</code> ，虽然是引用 C 库实现的，但是经典的两个算法（<code>lzo1x_1</code> 和 <code>lzo1x_999</code>）都提供了接口，貌似 Go 可以直接用了也就这一个库了。</p><p>发现这个库实现了 <code>io.Reader</code> 和 <code>io.Writer</code> 接口，<code>io.Reader</code> 读取压缩文件流，输出解压缩数据，<code>io.Writer</code> 实现输入原始数据，并写入到输入的 <code>io.Writer</code>。</p><p>想实现压缩数据流，看来需要使用 <code>io.Writer</code> 接口了，但是这个输入和输出都是 <code>io.Writer</code>，这可为难了，因为我们读取文件获得是 <code>io.Reader</code>，http 接口输入也是 <code>io.Reader</code>，貌似没有可以直接用的接口，没有办法实现了吗，不会我们自已封装一下，下面是封装的 <code>lzo</code> 数据流压缩方法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> lzo</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/cyberdelia/lzo&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Reader <span class="keyword">struct</span> &#123;</span><br><span class="line">r    io.Reader</span><br><span class="line">rb   []<span class="keyword">byte</span></span><br><span class="line">buff *bytes.Buffer</span><br><span class="line">lzo  *lzo.Writer</span><br><span class="line">err  error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewReader</span><span class="params">(r io.Reader)</span> *<span class="title">Reader</span></span> &#123;</span><br><span class="line">z := &amp;Reader&#123;</span><br><span class="line">r:    r,</span><br><span class="line">rb:   <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">256</span>*<span class="number">1024</span>),</span><br><span class="line">buff: bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">0</span>, <span class="number">256</span>*<span class="number">1024</span>)),</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed)</span><br><span class="line"><span class="keyword">return</span> z</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(z *Reader)</span> <span class="title">compress</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> z.err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> nr, nw <span class="keyword">int</span></span><br><span class="line">nr, z.err = z.r.Read(z.rb)</span><br><span class="line"><span class="keyword">if</span> z.err == io.EOF &#123;</span><br><span class="line"><span class="keyword">if</span> err := z.lzo.Close(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">z.err = err</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> nr &gt; <span class="number">0</span> &#123;</span><br><span class="line">nw, z.err = z.lzo.Write(z.rb[:nr])</span><br><span class="line"><span class="keyword">if</span> z.err == <span class="literal">nil</span> &amp;&amp; nr != nw &#123;</span><br><span class="line">z.err = io.ErrShortWrite</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(z *Reader)</span> <span class="title">Read</span><span class="params">(p []<span class="keyword">byte</span>)</span> <span class="params">(n <span class="keyword">int</span>, err error)</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> z.err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>, z.err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> z.buff.Len() &lt;= <span class="number">0</span> &#123;</span><br><span class="line">z.compress()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">n, err = z.buff.Read(p)</span><br><span class="line"><span class="keyword">if</span> err == io.EOF &#123;</span><br><span class="line">err = <span class="literal">nil</span></span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">z.err = err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(z *Reader)</span> <span class="title">Reset</span><span class="params">(r io.Reader)</span></span> &#123;</span><br><span class="line">z.r = r</span><br><span class="line">z.buff.Reset()</span><br><span class="line">z.err = <span class="literal">nil</span></span><br><span class="line">z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个库会固定消耗 512k 内存，并不是很大，我们需要创建一个读取 buf 和一个压缩缓冲 buf， 都是256k的大小，实际压缩缓冲的 buf 并不需要 256k，毕竟压缩后数据会比原始数据小，考虑空间并不是很大，直接分配 256k 避免运行时分配。</p><p>实现原理当 http 从输入的 <code>io.Reader</code> (实际就是我们上面封装的 <code>lzo</code> 库)， 读取数据时，这个库检查压缩缓冲是否为空，为空的情况会从文件读取 256k 数据并压缩输入到压缩缓冲中，然后从压缩缓冲读取数据给 http 的 <code>io.Reader</code>，如果压缩缓冲区有数据就直接从压缩缓冲区读取压缩数据。</p><p>这并不是线程安全的，并且固定分配 512k 的缓冲，所以也提供了一个 <code>Reset</code> 方法，来复用这个对象，避免重复分配内存，但是需要保证一个<code> lzo</code> 对象实例只能被一个 Goroutine 访问， 这可以使用 <code>sync.Pool</code> 来保证，下面的代码我用另一种方法来保证。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;os&quot;</span></span><br><span class="line"><span class="string">&quot;path/filepath&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"><span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;.../pkg/aliyun_oss&quot;</span></span><br><span class="line"><span class="string">&quot;.../pkg/lzo&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> oss *aliyun_oss.AliyunOSS</span><br><span class="line">files := os.Args[<span class="number">1</span>:]</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(files) &lt; <span class="number">1</span> &#123;</span><br><span class="line">fmt.Println(<span class="string">&quot;请输入要上传的文件&quot;</span>)</span><br><span class="line">os.Exit(<span class="number">1</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;待备份文件数量：%d\n&quot;</span>, <span class="built_in">len</span>(files))</span><br><span class="line"></span><br><span class="line">startTime := time.Now()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Printf(<span class="string">&quot;共耗时：%s\n&quot;</span>, time.Now().Sub(startTime).String())</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">n := <span class="number">4</span></span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">string</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 压缩日志</span></span><br><span class="line">wg.Add(n)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; n; i++ &#123;</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> wg.Done()</span><br><span class="line"><span class="keyword">var</span> compress *lzo.Reader</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> file := <span class="keyword">range</span> c &#123;</span><br><span class="line">r, err := os.Open(file)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> compress == <span class="literal">nil</span> &#123;</span><br><span class="line">compress = lzo.NewReader(r)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">compress.Reset(r)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">name := filepath.Base(file)</span><br><span class="line">err = oss.PutObject(<span class="string">&quot;tmp/&quot;</span>+name+<span class="string">&quot;1.lzo&quot;</span>, compress)</span><br><span class="line">r.Close()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> _, file := <span class="keyword">range</span> files &#123;</span><br><span class="line">c &lt;- file</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">close</span>(c)</span><br><span class="line">wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>程序为每个 Goroutine 分配一个固定的 <code>compress</code> ，当需要压缩文件的时候判断是创建还是重置，来达到复用的效果。</p><p>该程序运行输出:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">待备份文件数量：336</span><br><span class="line">共耗时 18m20.162441931s</span><br></pre></td></tr></table></figure><p>实际耗时比优化前提升了 28%, 实际通过 <code>iostat</code> 命令分析也发现，资源消耗也有了明显的改善，下面是 <code>iostat -m -x 5 10000</code> 命令采集各个阶段数据。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">          15.72    0.00    6.58   74.10    0.00    3.60</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.00    0.00    0.00     0.00     0.00     0.00     0.00    0.00    0.00    0.00   0.00   0.00</span><br><span class="line">vdb               3.80     3.40 1374.20    1.20 86484.00    18.40   125.79   121.57   87.24   87.32    1.00   0.73 100.00</span><br><span class="line"></span><br><span class="line">avg-cpu:  %user   %nice %system %iowait  %steal   %idle</span><br><span class="line">          26.69    0.00    8.42   64.27    0.00    0.62</span><br><span class="line"></span><br><span class="line">Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util</span><br><span class="line">vda               0.00     0.20  426.80    0.80  9084.80     4.00    42.51     2.69    6.29    6.30    1.00   0.63  26.92</span><br><span class="line">vdb               1.80     0.00 1092.60    0.00 72306.40     0.00   132.36   122.06  108.45  108.45    0.00   0.92 100.02</span><br></pre></td></tr></table></figure><p>通过 <code>iostat</code> 发现只有 <code>r_await</code>， <code>w_await</code> 被完全优化，<code>iowait</code> 有明显的改善，运行时间更短了，效率更高了，对 io 产生影响的时间也更短了。</p><h2 id="优化期间遇到的问题"><a href="#优化期间遇到的问题" class="headerlink" title="优化期间遇到的问题"></a>优化期间遇到的问题</h2><p>首先对找到的 <code>lzo</code> 算法库进行测试，确保压缩和解压缩没有问题，并且和 <code>lzop</code> 命令兼容。</p><p>在这期间发现使用压缩的数据比 <code>lzop</code> 压缩数据大了很多，之后阅读了源码实现，并没有发现任何问题，尝试调整缓冲区大小，发现对生成的压缩文件大小有明显改善。</p><p>这个发现让我也很为难，究竟多大的缓冲区合适呢，只能去看 <code>lzop</code> 的实现了，发现 lzop 默认压缩块大小为 256k, 实际 lzo 算法支持的最大块大小就是 256k，所以实现 <code>lzo</code> 算法包装是创建的是 256k 的缓冲区的，这个缓冲区的大小就是压缩块的大小，大家使用的时候建议不要调整了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这个方案上线之后，由原来需要近半分钟上传的，改善到大约只有十秒（Go 语言本身效率也有很大帮助），而且 load 有了明显的改善。</p><p>优化前每当运行日志备份，CPU 经常爆表，优化后备份时 CPU 增幅 20%，可以从容应对业务扩展问题了。</p><p>测试是在一台空闲的机器上进行的，实际生产服务器本身 <code>w_await</code> 会有 20 左右，如果使用固态硬盘，全双工模式，读和写是分离的，那么优化掉 <code>w_await</code> 对业务的帮助是非常大的，不会阻塞业务日志写通道了。</p><p>当然我们服务器是高速云盘(机械盘)，由于机械盘物理特征只能是半双工，要么读、要么写，所以优化掉 <code>w_await</code> 确实效率会提升很多，但是依然会对业务服务写有影响。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近遇到一个日志备份 io 过高的问题，业务日志每十分钟备份一次，本来是用 Python 写一个根据规则扫描备份日志问题不大，但是随着业务越来越多，单机上的日志文件越来越大，文件数量也越来越多，导致每每备份的瞬间 io 阻塞严重， CPU 和 load 异常的高，好在备份速度很快，对业务影响不是很大，这个问题会随着业务增长，越来越明显，这段时间抽空对备份方式做了优化，效果十分显著，整理篇文章记录一下。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="compress" scheme="https://blog.thinkeridea.com/tags/compress/"/>
    
    <category term="io" scheme="https://blog.thinkeridea.com/tags/io/"/>
    
    <category term="压缩" scheme="https://blog.thinkeridea.com/tags/%E5%8E%8B%E7%BC%A9/"/>
    
  </entry>
  
  <entry>
    <title>【Go】获取用户真实的ip地址</title>
    <link href="https://blog.thinkeridea.com/201903/go/get_client_ip.html"/>
    <id>https://blog.thinkeridea.com/201903/go/get_client_ip.html</id>
    <published>2019-03-27T23:02:57.000Z</published>
    <updated>2019-03-28T00:19:21.695Z</updated>
    
    <content type="html"><![CDATA[<p>用户请求到达提供服务的服务器中间有很多的环节，导致服务获取用户真实的 ip 非常困难，大多数的框架及工具库都会封装各种获取用户真实 ip 的方法，在  <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 包中也封装了各种 ip 相关的操作，其中就包含获取客户端 ip 的方法，比较实用的方法如下：</p><ul><li><code>func ClientIP(r *http.Request) string</code> ClientIP 尽最大努力实现获取客户端 IP 的算法。 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。</li><li><code>func ClientPublicIP(r *http.Request) string</code> ClientPublicIP 尽最大努力实现获取客户端公网 IP 的算法。 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。</li><li><code>func HasLocalIP(ip net.IP) bool</code> HasLocalIP 检测 IP 地址是否是内网地址</li><li><code>func HasLocalIPddr(ip string) bool</code> HasLocalIPddr 检测 IP 地址字符串是否是内网地址</li><li><code>func RemoteIP(r *http.Request) string</code> RemoteIP 通过 RemoteAddr 获取 IP 地址， 只是一个快速解析方法。</li></ul><a id="more"></a><h2 id="获取用户真实ip地址"><a href="#获取用户真实ip地址" class="headerlink" title="获取用户真实ip地址"></a>获取用户真实ip地址</h2><p><code>ClientIP</code> 方法 与 <code>ClientPublicIP</code> 方法的实现类似，只是一个按照 http 协议约定获取客户端 ip， 一个按照约定格式查找到公网 ip。</p><p>在网络与服务架构、业务逻辑复杂的环境中，按照 http 协议约定的方式，并非总能获取到真实的 ip，在我们的业务中用户流量经由三方多层级转发(都是三方自己实现的http client) ，难免会出现一些纰漏，这时越往后的服务获取用户真实 ip 越加困难，你甚至不知道自己获取的 ip 是否是真实的。</p><p>但是我们的客户经由三方转发而来的流量，那么客户极大多数甚至排除测试之外都是公网用户，结合使用 <code>ClientPublicIP</code> 和 <code>ClientIP</code> 方法总能更好的获取用户的真实 ip。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// var r *http.Request</span></span><br><span class="line">ip := exnet.ClientPublicIP(r)</span><br><span class="line"><span class="keyword">if</span> ip == <span class="string">&quot;&quot;</span>&#123;</span><br><span class="line">  ip = exnet.ClientIP(r)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用上面的方法总能有效的获取用户真实的 ip 地址，下面分析下两个方法的具体实现。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ClientIP 尽最大努力实现获取客户端 IP 的算法。</span></span><br><span class="line"><span class="comment">// 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ClientIP</span><span class="params">(r *http.Request)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">xForwardedFor := r.Header.Get(<span class="string">&quot;X-Forwarded-For&quot;</span>)</span><br><span class="line">ip := strings.TrimSpace(strings.Split(xForwardedFor, <span class="string">&quot;,&quot;</span>)[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">if</span> ip != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ip = strings.TrimSpace(r.Header.Get(<span class="string">&quot;X-Real-Ip&quot;</span>))</span><br><span class="line"><span class="keyword">if</span> ip != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ClientIP</code> 首先读取 <code>X-Forwarded-For</code> header 中用 <code>,</code> 分隔的第一个ip地址，如果这个地址不存在，就会从 <code>X-Real-Ip</code> header 中获取，如果还是不存在，说明流量并非是由反向代理转发而来，而是客户端直接请求服务，这时通过 <code>http.Request.RemoteAddr</code> 字段截取除去端口号的 ip 地址。</p><p>这个方法很简单，就是按照 http 约定的格式获取，其中 <code>X-Forwarded-For</code> 和 <code>X-Real-Ip</code> header 由反向代理填充，例如 nginx 或 haproxy。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ClientPublicIP 尽最大努力实现获取客户端公网 IP 的算法。</span></span><br><span class="line"><span class="comment">// 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ClientPublicIP</span><span class="params">(r *http.Request)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> ip <span class="keyword">string</span></span><br><span class="line"><span class="keyword">for</span> _, ip = <span class="keyword">range</span> strings.Split(r.Header.Get(<span class="string">&quot;X-Forwarded-For&quot;</span>), <span class="string">&quot;,&quot;</span>) &#123;</span><br><span class="line">ip = strings.TrimSpace(ip)</span><br><span class="line"><span class="keyword">if</span> ip != <span class="string">&quot;&quot;</span> &amp;&amp; !HasLocalIPddr(ip) &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ip = strings.TrimSpace(r.Header.Get(<span class="string">&quot;X-Real-Ip&quot;</span>))</span><br><span class="line"><span class="keyword">if</span> ip != <span class="string">&quot;&quot;</span> &amp;&amp; !HasLocalIPddr(ip) &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">if</span> !HasLocalIPddr(ip) &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ClientPublicIP</code> 很简单，和 <code>ClientIP</code> 方法的读取顺序一样，只是试图中 <code>X-Forwarded-For</code> 列表中找到一个公网ip，如果没有检查 <code>X-Real-Ip</code> 是否是一个公网 ip，其次检查 <code>http.Request.RemoteAddr</code> 是否是公网ip，如果没有找到公网 ip 这返回一个空字符串。</p><p>这个方法可以让我们有机会优先获取到用户的公网 ip，往往公网 ip 对我们来说更有价值。</p><h2 id="检查ip对否是内网地址"><a href="#检查ip对否是内网地址" class="headerlink" title="检查ip对否是内网地址"></a>检查ip对否是内网地址</h2><p> <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 中还提供了检查 ip 地址是否是内网地址，这在有些情况下非常有用，比如：服务中有些接口只能内网访问，也就是只允许管理员访问（例如动态设定日志级别、查看服务 pprof 信息）；我们想隐藏后端服务，只暴露给用户负载均衡(反向代理)，用户无法直接访问我们的服务，这些方法及其有用，下面看看具体实现。</p><p>我的服务提供了动态设置日志级别，以便服务出现问题，可以第一时间查看调试日志分析具体原因，但是这个接口很危险，不应该暴露给公网，所以会用路由中间件检查请求是否来自公网，来自公网则返回 404。</p><p>该方法认为如下地址段都是内网地址：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">10.0</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">8</span></span><br><span class="line"><span class="number">169.254</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">16</span></span><br><span class="line"><span class="number">172.16</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.17</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.18</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.19</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.20</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.21</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.22</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.23</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.24</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.25</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.26</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.27</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.28</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.29</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.30</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">172.31</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">12</span></span><br><span class="line"><span class="number">192.168</span><span class="number">.0</span><span class="number">.0</span>/<span class="number">16</span></span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// HasLocalIPddr 检测 IP 地址字符串是否是内网地址</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">HasLocalIPddr</span><span class="params">(ip <span class="keyword">string</span>)</span> <span class="title">bool</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> HasLocalIP(net.ParseIP(ip))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HasLocalIP 检测 IP 地址是否是内网地址</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">HasLocalIP</span><span class="params">(ip net.IP)</span> <span class="title">bool</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> _, network := <span class="keyword">range</span> localNetworks &#123;</span><br><span class="line"><span class="keyword">if</span> network.Contains(ip) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ip.IsLoopback()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两个检查方法实现差异仅接受参数类型不一致，检查过程都是逐个对比内网 ip 段是否包含该ip地址，如果不包含则判断该地址是否是回环地址。</p><h2 id="获取反向代理ip"><a href="#获取反向代理ip" class="headerlink" title="获取反向代理ip"></a>获取反向代理ip</h2><p>如何判断改地址来自反向代理服务器呢，不同的反向代理实现都有些差异，4 层反向代理甚至可以提供用户的真实 ip（<code>http.Request.RemoteAddr</code> 是用户的ip，而不是反向代理的）， 而隐藏自己的ip，这里说一下常见的方法。</p><p>往往 <code>http.Request.RemoteAddr</code> 保存最后一个连接服务的客户端 ip，我们获取反向代理的ip地址，最简单有效的方法就是通过 <code>http.Request.RemoteAddr</code>  获取， <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 中提供了 <code>RemoteIP</code> 的快捷方法，实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// RemoteIP 通过 RemoteAddr 获取 IP 地址， 只是一个快速解析方法。</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">RemoteIP</span><span class="params">(r *http.Request)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> ip</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个非常方便的脚手架，它仅仅切分 <code>http.Request.RemoteAddr</code> 的 ip 和端口，并返回有效的ip地址，但却可以简化我们的编写业务代码。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;用户请求到达提供服务的服务器中间有很多的环节，导致服务获取用户真实的 ip 非常困难，大多数的框架及工具库都会封装各种获取用户真实 ip 的方法，在  &lt;a href=&quot;https://github.com/thinkeridea/go-extend/blob/master/exnet&quot;&gt;exnet&lt;/a&gt; 包中也封装了各种 ip 相关的操作，其中就包含获取客户端 ip 的方法，比较实用的方法如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;func ClientIP(r *http.Request) string&lt;/code&gt; ClientIP 尽最大努力实现获取客户端 IP 的算法。 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func ClientPublicIP(r *http.Request) string&lt;/code&gt; ClientPublicIP 尽最大努力实现获取客户端公网 IP 的算法。 解析 X-Real-IP 和 X-Forwarded-For 以便于反向代理（nginx 或 haproxy）可以正常工作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func HasLocalIP(ip net.IP) bool&lt;/code&gt; HasLocalIP 检测 IP 地址是否是内网地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func HasLocalIPddr(ip string) bool&lt;/code&gt; HasLocalIPddr 检测 IP 地址字符串是否是内网地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func RemoteIP(r *http.Request) string&lt;/code&gt; RemoteIP 通过 RemoteAddr 获取 IP 地址， 只是一个快速解析方法。&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="ip" scheme="https://blog.thinkeridea.com/tags/ip/"/>
    
    <category term="net" scheme="https://blog.thinkeridea.com/tags/net/"/>
    
    <category term="client" scheme="https://blog.thinkeridea.com/tags/client/"/>
    
    <category term="RemoteAddr" scheme="https://blog.thinkeridea.com/tags/remoteaddr/"/>
    
  </entry>
  
  <entry>
    <title>【Go】IP地址转换：数字与字符串之间的转换</title>
    <link href="https://blog.thinkeridea.com/201903/go/ip2long.html"/>
    <id>https://blog.thinkeridea.com/201903/go/ip2long.html</id>
    <published>2019-03-24T23:34:57.000Z</published>
    <updated>2019-03-28T02:23:22.664Z</updated>
    
    <content type="html"><![CDATA[<p>IP 地址库中 IP 地址的保存格式一般有两种，一种是点分十进制形式(192.168.1.1)，另一种是数字形式(3232235777)，应用中，经常需要在这两种格式之间做转换。</p><p>针对这一个问题我在 <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 扩展包里面实现可两者的转换的快捷方法：</p><ul><li><code>func IP2Long(ip net.IP) (uint, error)</code>  IP2Long 把 net.IP 转为数值</li><li><code>func Long2IP(i uint) (net.IP, error)</code>  Long2IP 把数值转为 net.IP</li><li><code>func IPString2Long(ip string) (uint, error)</code> IPString2Long 把 ip 字符串转为数值</li><li><code>func Long2IPString(i uint) (string, error)</code> Long2IPString 把数值转为 ip 字符串</li></ul><p>使用示例：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;net&quot;</span></span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exnet&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">ip := <span class="string">&quot;192.168.1.1&quot;</span></span><br><span class="line"></span><br><span class="line">n, _ := exnet.IPString2Long(ip)</span><br><span class="line">s, _ := exnet.Long2IPString(n)</span><br><span class="line"></span><br><span class="line">fmt.Println(n, s == ip)</span><br><span class="line"></span><br><span class="line">Ip1 := net.ParseIP(ip) <span class="comment">// 会得到一个16字节的byte，主要为了兼容ipv6</span></span><br><span class="line">n, _ = exnet.IP2Long(Ip1)</span><br><span class="line"></span><br><span class="line">Ip2, _ := exnet.Long2IP(n)</span><br><span class="line"></span><br><span class="line">fmt.Println(n, reflect.DeepEqual(Ip1[<span class="number">12</span>:], Ip2))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><a id="more"></a><p>那么是如何将点分十进制的IP地址转为数字？<br>IPv4 地址有4个字节，样式如下：<br>MSB————–LSB<br>b4 b3 b2 b1</p><p>每个字节表示的范围：</p><ul><li>byte4: 4294967296(1&lt;&lt;32)</li><li>byte3: 16777216(1&lt;&lt;24)</li><li>byte2: 65536(1&lt;&lt;16)</li><li>byte1: 256(1&lt;&lt;8)</li></ul><p>通用公式：<code>b4&lt;&lt;24 | b3&lt;&lt;16 | b2&lt;&lt;8 | b1</code><br>例如，222.173.108.86<br>转换方法：<code>222&lt;&lt;24 | 173&lt;&lt;16 | 108&lt;&lt;8 | 86 = 3735907414</code><br>再例如，1.0.1.1<br>转换方法：<code>1&lt;&lt;24 | 0&lt;&lt;16 | 1&lt;&lt;8 | 1 = 16777473</code></p><p> <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 中实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// IPString2Long 把ip字符串转为数值</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IPString2Long</span><span class="params">(ip <span class="keyword">string</span>)</span> <span class="params">(<span class="keyword">uint</span>, error)</span></span> &#123;</span><br><span class="line">b := net.ParseIP(ip).To4()</span><br><span class="line"><span class="keyword">if</span> b == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>, errors.New(<span class="string">&quot;invalid ipv4 format&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">uint</span>(b[<span class="number">3</span>]) | <span class="keyword">uint</span>(b[<span class="number">2</span>])&lt;&lt;<span class="number">8</span> | <span class="keyword">uint</span>(b[<span class="number">1</span>])&lt;&lt;<span class="number">16</span> | <span class="keyword">uint</span>(b[<span class="number">0</span>])&lt;&lt;<span class="number">24</span>, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把数值转换为字符串的逻辑翻转过来即可， <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet">exnet</a> 中实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Long2IPString 把数值转为ip字符串</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Long2IPString</span><span class="params">(i <span class="keyword">uint</span>)</span> <span class="params">(<span class="keyword">string</span>, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> i &gt; math.MaxUint32 &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span>, errors.New(<span class="string">&quot;beyond the scope of ipv4&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ip := <span class="built_in">make</span>(net.IP, net.IPv4len)</span><br><span class="line">ip[<span class="number">0</span>] = <span class="keyword">byte</span>(i &gt;&gt; <span class="number">24</span>)</span><br><span class="line">ip[<span class="number">1</span>] = <span class="keyword">byte</span>(i &gt;&gt; <span class="number">16</span>)</span><br><span class="line">ip[<span class="number">2</span>] = <span class="keyword">byte</span>(i &gt;&gt; <span class="number">8</span>)</span><br><span class="line">ip[<span class="number">3</span>] = <span class="keyword">byte</span>(i)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ip.String(), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;IP 地址库中 IP 地址的保存格式一般有两种，一种是点分十进制形式(192.168.1.1)，另一种是数字形式(3232235777)，应用中，经常需要在这两种格式之间做转换。&lt;/p&gt;
&lt;p&gt;针对这一个问题我在 &lt;a href=&quot;https://github.com/thinkeridea/go-extend/blob/master/exnet&quot;&gt;exnet&lt;/a&gt; 扩展包里面实现可两者的转换的快捷方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;func IP2Long(ip net.IP) (uint, error)&lt;/code&gt;  IP2Long 把 net.IP 转为数值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func Long2IP(i uint) (net.IP, error)&lt;/code&gt;  Long2IP 把数值转为 net.IP&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func IPString2Long(ip string) (uint, error)&lt;/code&gt; IPString2Long 把 ip 字符串转为数值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func Long2IPString(i uint) (string, error)&lt;/code&gt; Long2IPString 把数值转为 ip 字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;18&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;19&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;20&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;21&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;22&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;23&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;24&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;25&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;package&lt;/span&gt; main&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;string&quot;&gt;&amp;quot;fmt&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;string&quot;&gt;&amp;quot;net&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;string&quot;&gt;&amp;quot;reflect&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;string&quot;&gt;&amp;quot;github.com/thinkeridea/go-extend/exnet&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;()&lt;/span&gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	ip := &lt;span class=&quot;string&quot;&gt;&amp;quot;192.168.1.1&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	n, _ := exnet.IPString2Long(ip)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	s, _ := exnet.Long2IPString(n)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	fmt.Println(n, s == ip)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	Ip1 := net.ParseIP(ip) &lt;span class=&quot;comment&quot;&gt;// 会得到一个16字节的byte，主要为了兼容ipv6&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	n, _ = exnet.IP2Long(Ip1)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	Ip2, _ := exnet.Long2IP(n)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	fmt.Println(n, reflect.DeepEqual(Ip1[&lt;span class=&quot;number&quot;&gt;12&lt;/span&gt;:], Ip2))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="ip" scheme="https://blog.thinkeridea.com/tags/ip/"/>
    
    <category term="net" scheme="https://blog.thinkeridea.com/tags/net/"/>
    
    <category term="ip2long" scheme="https://blog.thinkeridea.com/tags/ip2long/"/>
    
    <category term="ip2number" scheme="https://blog.thinkeridea.com/tags/ip2number/"/>
    
  </entry>
  
  <entry>
    <title>【Go】go get 自动代理</title>
    <link href="https://blog.thinkeridea.com/201903/go/go_get_proxy.html"/>
    <id>https://blog.thinkeridea.com/201903/go/go_get_proxy.html</id>
    <published>2019-03-20T23:34:57.000Z</published>
    <updated>2019-03-22T02:15:33.706Z</updated>
    
    <content type="html"><![CDATA[<p>最近发现技术交流群里很多人在询问 go get 墙外包失败的问题，大家给了很多解决方案：</p><ul><li>从 Github 的代码库 clone</li><li>设置 GOPROXY 环境变量配置代理, 例如：GOPROXY=<a href="https://goproxy.io/">https://goproxy.io</a></li><li>配置命令行代理，https_proxy 环境变量</li><li>使用 go mod replace</li><li>使用 Gopm 类似的工具</li><li>……</li></ul><p>Go 的社区很活跃，国内 gopher 对 Go 的热情不会因为墙的存在而减少，从社区想到这么多翻墙方案就能看出来了。</p><p>上面的方法都是可行的，但是总有一些不尽人意，社区也一直在找更好的方法，我一直使用自动代理的方式获取墙外的包，可以支持所有 Go 原生拉取包操作，比如 go get、go mod、dep、godep、glide 等各种方法，只需要配置一次，只要在任何原生命令前加前缀运行命令即可，效率很高。</p><a id="more"></a><h2 id="实战操作"><a href="#实战操作" class="headerlink" title="实战操作"></a>实战操作</h2><p>工具类就先不讲原理，想直接获取方法的同学看这一部分即可，想了解原理的同学可以看后面的<a href="#%E5%8E%9F%E7%90%86%E7%AF%87">原理部分</a>。</p><h4 id="你需要准备如下工具："><a href="#你需要准备如下工具：" class="headerlink" title="你需要准备如下工具："></a>你需要准备如下工具：</h4><ul><li>一个 http 代理</li><li>Git</li><li>Github 账号设置好 ssh</li><li>其它 git 相关服务设置好 ssh (例如自建 gitlab)</li><li>一个可以运行shell的环境 (linux、Mac、windows 可以使用 git bash)</li></ul><h4 id="具体步骤"><a href="#具体步骤" class="headerlink" title="具体步骤"></a>具体步骤</h4><ul><li><p>首先通过 git 设置需要不代理的网站，以 Github 为例，执行 <code>git config --global url.git@github.com:.insteadof https://github.com/</code> 从 https 转到 ssh 协议，这样会使我们设置的 https 代理不作用在 ssh 协议上，如果有自建的服务只要更换地址就可以了。</p></li><li><p>新建一个脚本 (proxy)，修改里面的代理地址为自己的代理地址，如下：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env bash </span></span><br><span class="line"><span class="comment"># 如果你的系统没有bash，或者没有 /usr/bin/env ，请修改上一行指令为你的环境</span></span><br><span class="line">export http_proxy=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">1087</span>  <span class="comment"># 代理地址需要换成自己的</span></span><br><span class="line">export https_proxy=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">1087</span></span><br><span class="line">export ftp_proxy=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">1087</span></span><br><span class="line"></span><br><span class="line">exec <span class="variable">$</span>&#123;<span class="selector-tag">@</span>:<span class="number">1</span>&#125;</span><br></pre></td></tr></table></figure></li><li><p>给 shell 脚本设置可执行权限，然后放到 path 环境变量路径下。</p></li><li><p>测试 <code>proxy curl https://www.google.com</code> 和 <code>curl https://www.google.com</code> 第一个命令可以获取到结果，第二个命令不可以。</p></li><li><p>测试 <code>proxy go get -v golang.org/x/text/…</code> 可以正常下载包，其它任何拉取包命令都可以添加 <code>proxy</code> 前缀执行 ，比如 <code>proxy dep ensure -v</code></p></li></ul><p>截止当前你就配置了一个 <code>go get</code> 自动代理的环境，以后需要翻墙操作的指令运行时加 <code>proxy</code> 就可以了，该方法并不只适用于 <code>go get</code>，任何需要命令行代理都可以使用。</p><h2 id="原理篇"><a href="#原理篇" class="headerlink" title="原理篇"></a>原理篇</h2><p>实际原理简单，找到这种方法也是一种巧合，在入坑 Go 之前我经常用 linux，当时有一些需求需要命令行翻墙，找到了三个环境变量 <code>http_proxy</code>、<code>https_proxy</code>、<code>ftp_proxy</code>，但是全局设置导致很多请求变慢，如果在一个窗口临时设置就导致需要记住那个窗口设置了代理，切换窗口成本也比较高，后来根据 shell 的特性，任何一个脚本都有自己独立的环境变量，所以用一个脚本设置代理环境变量，<code>exec $&#123;@:1&#125;</code> 可以执行脚本后面的指令，也就是我们实际需要运行的指令，这样在需要代理的命令前就加上这个脚本前缀就好了，单行命令代理就这么简单的配置好了。</p><p>前期我使用 go 的时候遇到下载不了的包时，就会在 <code>go get</code> 前加上 <code>proxy</code> 指令，但是我发现拉取 Github 包的效率非常低，本身国内现在访问 Github 已经很快了。也是一个巧合，当时我公司 Go 项目迁移到 Github 上，所有项目全部是私有项目，有同事提供了一个 git https 转 ssh 协议的操作，<code>git config --global url.git@….:.insteadof https://…./</code>，这个操作让我看到一个隐性福利，之前的代理只会代理 https 并不能代理 ssh 协议，那么使用这个指令把不需要代理的网站全部转成 ssh 协议，然后加上 <code>proxy</code> 运行 <code>go get</code> 就成了自动代理了，尝试之后确实效率很高，至此一直使用到今天。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近发现技术交流群里很多人在询问 go get 墙外包失败的问题，大家给了很多解决方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 Github 的代码库 clone&lt;/li&gt;
&lt;li&gt;设置 GOPROXY 环境变量配置代理, 例如：GOPROXY=&lt;a href=&quot;https://goproxy.io/&quot;&gt;https://goproxy.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;配置命令行代理，https_proxy 环境变量&lt;/li&gt;
&lt;li&gt;使用 go mod replace&lt;/li&gt;
&lt;li&gt;使用 Gopm 类似的工具&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 的社区很活跃，国内 gopher 对 Go 的热情不会因为墙的存在而减少，从社区想到这么多翻墙方案就能看出来了。&lt;/p&gt;
&lt;p&gt;上面的方法都是可行的，但是总有一些不尽人意，社区也一直在找更好的方法，我一直使用自动代理的方式获取墙外的包，可以支持所有 Go 原生拉取包操作，比如 go get、go mod、dep、godep、glide 等各种方法，只需要配置一次，只要在任何原生命令前加前缀运行命令即可，效率很高。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="proxy" scheme="https://blog.thinkeridea.com/tags/proxy/"/>
    
    <category term="git" scheme="https://blog.thinkeridea.com/tags/git/"/>
    
    <category term="shell" scheme="https://blog.thinkeridea.com/tags/shell/"/>
    
  </entry>
  
  <entry>
    <title>【Go】那么多数值类型，应该选哪个？</title>
    <link href="https://blog.thinkeridea.com/201903/go/selection_of_numerical_types.html"/>
    <id>https://blog.thinkeridea.com/201903/go/selection_of_numerical_types.html</id>
    <published>2019-03-18T23:34:57.000Z</published>
    <updated>2019-03-19T01:00:46.558Z</updated>
    
    <content type="html"><![CDATA[<p><code>Go</code> 内置很多种数值类型，往往初学者不知道编写程序如何选择，使用哪种数值类型更有优势。</p><p>内置的数值类型有：<code>uint8</code>、 <code>uint16</code>、 <code>uint32</code>、 <code>uint64</code>、 <code>uint</code>、 <code>int8</code>、 <code>int16</code>、 <code>int32</code>、 <code>int64</code>、 <code>int</code>。</p><p>从类型名称上可以很好了解到类型的大小，这个非常直观，<code>uint</code> 和 <code>int</code> 这两种类型是不带大小的，那么它们的大小会根据编译参数 <code>GOARCH=amd64</code> 平台决定的。</p><p>我最早设计的一个go的项目，当时设计系统使用采用最小类型原则，几乎使用了大多数数值类型，很少使用 <code>uint</code> 和 <code>int</code> 类型，后来遇到很多问题，标准库和三方库函数都接收 <code>int</code>、 <code>uint</code>、 <code>int64</code>、<code>uint64</code>, 一些代码生成工具, 比如 <code>protobuf</code> 生成类型是 <code>int32</code>，一些三方系统大多数也是 <code>int</code> 类型，这时候与其它组件件的交互就需要 <span style="color:red">类型转换</span>, 类型转换成本是很高的，导致程序性能并没有预期的好。</p><a id="more"></a><p>上面一个小故事(事故)警醒大家不要一味的根据数据的大小选择数值类型，而要考虑数值的用来做什么，后面会有哪些交互，需要调用哪些函数等等，是不是选择数值具体使用什么类型很复杂呢？并不是这样，考虑的越少，选择越简单，下面有一些近些年的总结。</p><ul><li>需要原子操作的数值根据数据大小选择 <code>int32</code>、 <code>int64</code>、 <code>uint32</code>、<code>uint64</code>。因为原子类型的操作包天生支持这些类型。</li><li>需要与代码生成的交互的数据，可以看生成的代码具体使用哪种类型，做一下参考。</li><li>需要调用大多数标准库函数进行处理，选这个 <code>int</code> (我们的程序大多数跑在64位系统上，如果运行在32系统，且类型可能会超过 <code>int32</code> 可以选择 <code>int64</code>) 。</li><li>有些时候可能我们需要一个无符号数据且比较大优先选用 <code>uint</code> 和 <code>uint64</code> 。</li><li>只和自己的函数交互以及一些不关注具体类型的包(<code>json</code>、<code>fmt</code>)交互式时，按数值使用范围选择最小类型。</li></ul><p>我现在写代码一些特殊场景如原子操作会针对使用的包选择具体类型，偶尔会使用<code>uint64</code>，往往是一些按位做一些复杂计算的数据，也都局限在局部逻辑上，与其它模块或者系统交互的都会使用 <code>int</code> 类型，这样可以大幅度降低数值类型的类型转换问题，从而从空间换取时间，获得更好的程序性能。</p><p>不得不说说 <code>Go</code> 语言神奇的 <code>int</code> 类型，为什么需要这样一个编程是无法确定具体长度的类型呢，而需要在编译时确定呢，有什么好处呢。</p><p>往往我们写程序是不太关注数值类型的，或者说我们程序中很多数值不会超过 <code>int32</code> 的最大值（往往我们的程序运行在 32 或 64位平台上），这个时候很多三方库都可以使用 <code>int</code> 作为交互类型，不用把一个函数为每种类型数值都写一遍，能简化标准库。我们也能写出更容易维护、简洁的系统。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;code&gt;Go&lt;/code&gt; 内置很多种数值类型，往往初学者不知道编写程序如何选择，使用哪种数值类型更有优势。&lt;/p&gt;
&lt;p&gt;内置的数值类型有：&lt;code&gt;uint8&lt;/code&gt;、 &lt;code&gt;uint16&lt;/code&gt;、 &lt;code&gt;uint32&lt;/code&gt;、 &lt;code&gt;uint64&lt;/code&gt;、 &lt;code&gt;uint&lt;/code&gt;、 &lt;code&gt;int8&lt;/code&gt;、 &lt;code&gt;int16&lt;/code&gt;、 &lt;code&gt;int32&lt;/code&gt;、 &lt;code&gt;int64&lt;/code&gt;、 &lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从类型名称上可以很好了解到类型的大小，这个非常直观，&lt;code&gt;uint&lt;/code&gt; 和 &lt;code&gt;int&lt;/code&gt; 这两种类型是不带大小的，那么它们的大小会根据编译参数 &lt;code&gt;GOARCH=amd64&lt;/code&gt; 平台决定的。&lt;/p&gt;
&lt;p&gt;我最早设计的一个go的项目，当时设计系统使用采用最小类型原则，几乎使用了大多数数值类型，很少使用 &lt;code&gt;uint&lt;/code&gt; 和 &lt;code&gt;int&lt;/code&gt; 类型，后来遇到很多问题，标准库和三方库函数都接收 &lt;code&gt;int&lt;/code&gt;、 &lt;code&gt;uint&lt;/code&gt;、 &lt;code&gt;int64&lt;/code&gt;、&lt;code&gt;uint64&lt;/code&gt;, 一些代码生成工具, 比如 &lt;code&gt;protobuf&lt;/code&gt; 生成类型是 &lt;code&gt;int32&lt;/code&gt;，一些三方系统大多数也是 &lt;code&gt;int&lt;/code&gt; 类型，这时候与其它组件件的交互就需要 &lt;span style=&quot;color:red&quot;&gt;类型转换&lt;/span&gt;, 类型转换成本是很高的，导致程序性能并没有预期的好。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="int" scheme="https://blog.thinkeridea.com/tags/int/"/>
    
    <category term="numerical" scheme="https://blog.thinkeridea.com/tags/numerical/"/>
    
    <category term="uintptr" scheme="https://blog.thinkeridea.com/tags/uintptr/"/>
    
  </entry>
  
  <entry>
    <title>【Go】string 优化误区及建议</title>
    <link href="https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html"/>
    <id>https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html</id>
    <published>2019-02-22T22:34:57.000Z</published>
    <updated>2019-02-26T03:20:37.204Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p> 本文原标题为 《string 也是引用类型》，经过 <strong>郝林</strong> 大佬指点原标题存在诱导性，这里解释一下 “引用类型” 有两个特征：1、多个变量引用一块内存数据，不创建变量的副本，2、修改任意变量的数据，其它变量可见。显然字符串只满足了 “引用类型” 的第一个特点，不能满足第二个特点，顾不能说字符串是引用类型，感谢大佬指正。</p></blockquote><p>初学 <code>Go</code> 语言的朋友总会在传 <code>[]byte</code> 和 <code>string</code> 之间有着很多纠结，实际上是没有了解 <code>string</code> 与 <code>slice</code> 的本质，而且读了一些程序源码，也发现很多与之相关的问题，下面类似的代码估计很多初学者都写过，也充分说明了作者当时内心的纠结：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;bytes&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">xx</span><span class="params">(s []<span class="keyword">byte</span>)</span> []<span class="title">byte</span></span>&#123;</span><br><span class="line">    ....</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    s := <span class="string">&quot;xxx&quot;</span></span><br><span class="line">    </span><br><span class="line">    s = <span class="keyword">string</span>(xx([]<span class="keyword">byte</span>(s)))</span><br><span class="line">    </span><br><span class="line">    s = <span class="keyword">string</span>(bytes.Replace([]<span class="keyword">byte</span>(s), []<span class="keyword">byte</span>(<span class="string">&quot;x&quot;</span>), []<span class="keyword">byte</span>(<span class="string">&quot;&quot;</span>), <span class="number">-1</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然这样的代码并不是来自真实的项目，但是确实有人这样设计，单从设计上看就很糟糕了，这样设计的原因很多人说：“<code>slice</code> 是引用类型，传递引用类型效率高呀”，主要原因不了解两者的本质。</p><p>上面这个例子如果觉得有点基础和可爱，下面这个例子貌似并不那么容易说明其存在的问题了吧。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">xx</span><span class="params">(s *<span class="keyword">string</span>)</span> *<span class="title">string</span></span>&#123;</span><br><span class="line">    ....</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    s := <span class="string">&quot;xx&quot;</span></span><br><span class="line">    </span><br><span class="line">    s = *xx(&amp;s)</span><br><span class="line">    </span><br><span class="line">    ss :=[]*<span class="keyword">string</span>&#123;&#125;</span><br><span class="line">    </span><br><span class="line">    ss = <span class="built_in">append</span>(ss, &amp;s)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>指针效率高，我就用指针多好，可以减少内存分配呀，设计函数都接收指针变量，程序性能会有很大提升，在实际的项目中这种例子也不少见，我想通过这篇文档来帮助初学者走出误区，减少适得其反的优化技巧。</p><a id="more"></a><h2 id="slice-的定义"><a href="#slice-的定义" class="headerlink" title="slice 的定义"></a>slice 的定义</h2><p>在之前 “<a href="https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html">【Go】深入剖析slice和array</a>” 一文中说了 <code>slice</code> 在内存中的存储模式，<code>slice</code> 本身包含一个指向底层数组的指针，一个 <code>int</code> 类型的长度和一个 <code>int</code> 类型的容量， 这就是 <code>slice</code> 的本质， <code>[]byte</code> 本身也是一个 <code>slice</code>，只是底层数组存储的元素是 <code>byte</code>。下面这个图就是 <code>slice</code> 的在内存中的状态：</p><p><img src="/assets/image/20190111/slice_1.jpg"></p><p>看一下 <code>reflect.SliceHeader</code> 如何定义 <code>slice</code> 在内存中的结构吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> SliceHeader <span class="keyword">struct</span> &#123;</span><br><span class="line">Data <span class="keyword">uintptr</span></span><br><span class="line">Len  <span class="keyword">int</span></span><br><span class="line">Cap  <span class="keyword">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>slice</code> 是引用类型是 <code>slice</code> 本身会包含一个地址，在传递 <code>slice</code> 时只需要分配 <code>SliceHeader</code> 就好了， 而 <code>SliceHeader</code> 只包含了三个 <code>int</code> 类型，相当于传递一个 <code>slice</code> 就只需要拷贝 <code>SliceHeader</code>，而不用拷贝整个底层数组，所以才说 <code>slice</code> 是引用类型的。</p><p>那么字符串呢，计算机中我们处理的大多数问题都和字符串有关，难道传递字符串真的需要那么高的成本，需要借助 <code>slice</code> 和指针来减少内存开销吗。</p><h2 id="string-的定义"><a href="#string-的定义" class="headerlink" title="string 的定义"></a>string 的定义</h2><p><code>reflect</code> 包里面也定义了一个 <code>StringHeader</code> 看一下吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> StringHeader <span class="keyword">struct</span> &#123;</span><br><span class="line">Data <span class="keyword">uintptr</span></span><br><span class="line">Len  <span class="keyword">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>字符串只包含了两个 <code>int</code> 类型的数据，其中一个是指针，一个是字符串的长度，从 <code>StringHeader</code> 定义来看 <code>string</code> 并不会发生拷贝的，传递 <code>string</code> 只会拷贝 <code>StringHeader</code> 而已。</p><p>借助 <code>unsafe</code> 来分析一下情况是不是这样吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/davecgh/go-spew/spew&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">xx</span><span class="params">(s <span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">sh := *(*reflect.StringHeader)(unsafe.Pointer(&amp;s))</span><br><span class="line">spew.Dump(sh)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="string">&quot;xx&quot;</span></span><br><span class="line"></span><br><span class="line">sh := *(*reflect.StringHeader)(unsafe.Pointer(&amp;s))</span><br><span class="line">spew.Dump(sh)</span><br><span class="line"></span><br><span class="line">xx(s)</span><br><span class="line">xx(s[:<span class="number">1</span>])</span><br><span class="line">xx(s[<span class="number">1</span>:])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面这段代码的输出如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ee0,</span><br><span class="line"> Len: (int) 2</span><br><span class="line">&#125;</span><br><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ee0,</span><br><span class="line"> Len: (int) 2</span><br><span class="line">&#125;</span><br><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ee0,</span><br><span class="line"> Len: (int) 1</span><br><span class="line">&#125;</span><br><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ee1,</span><br><span class="line"> Len: (int) 1</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现前三个输出的指针都是同一个地址，第四个的地址发生了一个字节的偏移，分析来看传递字符串确实没有分配新的内存，同时和 <code>slice</code> 一样即使传递字符串的子串也不会分配新的内存空间，而是指向原字符串的中的一个位置。</p><p>这样说来把 <code>string</code> 转成 <code>[]byte</code> 还浪费的一个 <code>int</code> 的空间呢，需要分配更多的内存，真是适得其反呀，而且类型转换会发生内存拷贝，从 <code>string</code> 转为 <code>[]byte</code> 才是真的把 <code>string</code> 底层数据全部拷贝一遍呢，真是得不偿失呀。</p><h2 id="string-的两个小特性"><a href="#string-的两个小特性" class="headerlink" title="string 的两个小特性"></a>string 的两个小特性</h2><p>字符串还有两个小特性，针对字面量(就是直接写在程序中的字符串)，会创建在只读空间上，并且被复用，看一下下面的一个小例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/davecgh/go-spew/spew&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := <span class="string">&quot;xx&quot;</span></span><br><span class="line">b := <span class="string">&quot;xx&quot;</span></span><br><span class="line">c := <span class="string">&quot;xxx&quot;</span></span><br><span class="line">spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&amp;a)))</span><br><span class="line">spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&amp;b)))</span><br><span class="line">spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&amp;c)))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从输出可以了解到，相同的字面量会被复用，但是子串是不会复用空间的，这就是编译器给我们带来的福利了，可以减少字面量字符串占用的内存空间。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ea0,</span><br><span class="line"> Len: (int) 2</span><br><span class="line">&#125;</span><br><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5ea0,</span><br><span class="line"> Len: (int) 2</span><br><span class="line">&#125;</span><br><span class="line">(reflect.StringHeader) &#123;</span><br><span class="line"> Data: (uintptr) 0x10f5f2e,</span><br><span class="line"> Len: (int) 3</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>另一个小特性大家都知道，就是字符串是不能修改的，如果我们不希望调用函数修改我们的数据，最好传递字符串，高效有安全。</p><p>不过有了 <code>unsafe</code> 这个黑魔法，字符串的这一个特性也就不那么可靠了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">a := strings.Repeat(<span class="string">&quot;x&quot;</span>, <span class="number">10</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(a)</span><br><span class="line">strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&amp;a))</span><br><span class="line"></span><br><span class="line">sliceHeader := reflect.SliceHeader&#123;</span><br><span class="line">Data: strHeader.Data,</span><br><span class="line">Len:  strHeader.Len,</span><br><span class="line">Cap:  strHeader.Len,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b := *(*[]<span class="keyword">byte</span>)(unsafe.Pointer(&amp;sliceHeader))</span><br><span class="line"></span><br><span class="line">b[<span class="number">1</span>] = <span class="string">&#x27;a&#x27;</span></span><br><span class="line"></span><br><span class="line">fmt.Println(a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从输出里面居然发现字符串被修改了, 我们没有办法直接修改字符串，但是可以利用 <code>slice</code> 和 <code>string</code> 本身结构的特性，创建一个 <code>slice</code> 让它的指针指向 <code>string</code> 的指针位置，然后借助 <code>unsafe</code> 把这个 <code>SliceHeader</code> 转成 <code>[]byte</code> 来修改字符串，字符串确实被修改了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">xxxxxxxxxx</span><br><span class="line">xaxxxxxxxx</span><br></pre></td></tr></table></figure><p>看了上面的例子是不是开始担心把字符串传给其它函数真的不会更改吗？感觉很不放心的样子，难道使用任何函数都要了解它的内部实现吗，其实这种情况极少发生，还记得之前说的那个字符串特性吗，字面量字符串会放到只读空间中，这个很重要，可以保证不是任何函数想修改我们的字符串就可以修改的。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="built_in">recover</span>()</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line">a := <span class="string">&quot;xx&quot;</span></span><br><span class="line"></span><br><span class="line">strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&amp;a))</span><br><span class="line">sliceHeader := reflect.SliceHeader&#123;</span><br><span class="line">Data: strHeader.Data,</span><br><span class="line">Len:  strHeader.Len,</span><br><span class="line">Cap:  strHeader.Len,</span><br><span class="line">&#125;</span><br><span class="line">b := *(*[]<span class="keyword">byte</span>)(unsafe.Pointer(&amp;sliceHeader))</span><br><span class="line">b[<span class="number">1</span>] = <span class="string">&#x27;a&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行上面的代码发生了一个运行时不可修复的错误，就是这个特性其它函数不能确保输入字符串是否是字面量，也是不会恶意修改我们字符串的了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">unexpected fault address 0x1095dd5</span><br><span class="line">fatal error: fault</span><br><span class="line">[signal SIGBUS: bus error code&#x3D;0x2 addr&#x3D;0x1095dd5 pc&#x3D;0x106c804]</span><br><span class="line"></span><br><span class="line">goroutine 1 [running]:</span><br><span class="line">runtime.throw(0x1095fde, 0x5)</span><br><span class="line">&#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;panic.go:608 +0x72 fp&#x3D;0xc000040700 sp&#x3D;0xc0000406d0 pc&#x3D;0x10248d2</span><br><span class="line">runtime.sigpanic()</span><br><span class="line">&#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;signal_unix.go:387 +0x2d7 fp&#x3D;0xc000040750 sp&#x3D;0xc000040700 pc&#x3D;0x1037677</span><br><span class="line">main.main()</span><br><span class="line">&#x2F;Users&#x2F;qiyin&#x2F;project&#x2F;go&#x2F;src&#x2F;github.com&#x2F;yumimobi&#x2F;test&#x2F;a.go:22 +0x84 fp&#x3D;0xc000040798 sp&#x3D;0xc000040750 pc&#x3D;0x106c804</span><br><span class="line">runtime.main()</span><br><span class="line">&#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;proc.go:201 +0x207 fp&#x3D;0xc0000407e0 sp&#x3D;0xc000040798 pc&#x3D;0x1026247</span><br><span class="line">runtime.goexit()</span><br><span class="line">&#x2F;usr&#x2F;local&#x2F;go&#x2F;src&#x2F;runtime&#x2F;asm_amd64.s:1333 +0x1 fp&#x3D;0xc0000407e8 sp&#x3D;0xc0000407e0 pc&#x3D;0x104da51</span><br></pre></td></tr></table></figure><p><strong>关于字符串转 <code>[]byte</code> 在 <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 扩展包中有直接的实现，这种用法在 <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 内部方法实现中也有大量使用， 实际上因为原数据类型和处理数据的函数类型不一致，使用这种方法转换字符串和 <code>[]byte</code> 可以极大的提升程序性能</strong></p><ul><li><a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#ToString">exbytes.ToString</a> 零成本的把 <code>[]byte</code> 转为 <code>string</code>。</li><li><a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings#UnsafeToBytes">exstrings.UnsafeToBytes</a> 零成本的把 <code>[]byte</code> 转为 <code>string</code>。</li></ul><p>上面这两个函数用的好，可以极大的提升我们程序的性能，关于 <code>exstrings.UnsafeToBytes</code> 我们转换不确定是否是字面量的字符串时就需要确保调用的函数不会修改我们的数据，这往常在调用 <code>bytes</code> 里面的方法十分有效。</p><h2 id="传字符串和字符串指针的区别"><a href="#传字符串和字符串指针的区别" class="headerlink" title="传字符串和字符串指针的区别"></a>传字符串和字符串指针的区别</h2><p>之前分析了传递 <code>slice</code> 并没有 <code>string</code> 高效，何况转换数据类型本身就会发生数据拷贝。</p><p>那么在这篇文章的第二个例子，为什么说传递字符串指针也不好呢，要了解指针在底层就是一个 <code>int</code> 类型的数据，而我们字符串只是两个 <code>int</code> 而已，另外如果了解 <code>GC</code> 的话，<code>GC</code> 只处理堆上的数据，传递指针字符串会导致数据逃逸到堆上，阅读标准库的代码会有很多注释说明避免逃逸到堆上，这样会极大的增加 <code>GC</code> 的开销，<code>GC</code> 的成本可谓是很高的呀。</p><h2 id="疑惑"><a href="#疑惑" class="headerlink" title="疑惑"></a>疑惑</h2><p>这篇文章说 “传递 <code>slice</code> 并没有 <code>string</code> 高效”，为什么还会有 <code>bytes</code> 包的存在呢，其中很多函数的功能和 <code>strings</code> 包的功能一致，只是把 <code>string</code> 换成了 <code>[]byte</code>， 既然传递 <code>[]byte</code> 没有 <code>string</code> 效率好，这个包存在的意义是什么呢。</p><p>我们想一下转换数据类型是会发生数据拷贝，这个成本可是大的多呀，如果我们数据本身就是 <code>[]byte</code> 类型，使用 <code>strings</code> 包就需要转换数据类型了。</p><p>另外我们对比两个函数来看下一下即使传递 <code>[]byte</code> 没有 <code>string</code> 效率好，但是标准库实现上却会导致两个函数有很大的性能差异的。</p><p><code>strings.Repeat</code> 函数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Repeat</span><span class="params">(s <span class="keyword">string</span>, count <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="comment">// Since we cannot return an error on overflow,</span></span><br><span class="line"><span class="comment">// we should panic if the repeat will generate</span></span><br><span class="line"><span class="comment">// an overflow.</span></span><br><span class="line"><span class="comment">// See Issue golang.org/issue/16237</span></span><br><span class="line"><span class="keyword">if</span> count &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;strings: negative Repeat count&quot;</span>)</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> count &gt; <span class="number">0</span> &amp;&amp; <span class="built_in">len</span>(s)*count/count != <span class="built_in">len</span>(s) &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;strings: Repeat count causes overflow&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="built_in">len</span>(s)*count)</span><br><span class="line">bp := <span class="built_in">copy</span>(b, s)</span><br><span class="line"><span class="keyword">for</span> bp &lt; <span class="built_in">len</span>(b) &#123;</span><br><span class="line"><span class="built_in">copy</span>(b[bp:], b[:bp])</span><br><span class="line">bp *= <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">string</span>(b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>bytes.Repeat</code> 函数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Repeat</span><span class="params">(b []<span class="keyword">byte</span>, count <span class="keyword">int</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="comment">// Since we cannot return an error on overflow,</span></span><br><span class="line"><span class="comment">// we should panic if the repeat will generate</span></span><br><span class="line"><span class="comment">// an overflow.</span></span><br><span class="line"><span class="comment">// See Issue golang.org/issue/16237.</span></span><br><span class="line"><span class="keyword">if</span> count &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;bytes: negative Repeat count&quot;</span>)</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> count &gt; <span class="number">0</span> &amp;&amp; <span class="built_in">len</span>(b)*count/count != <span class="built_in">len</span>(b) &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;bytes: Repeat count causes overflow&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">nb := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="built_in">len</span>(b)*count)</span><br><span class="line">bp := <span class="built_in">copy</span>(nb, b)</span><br><span class="line"><span class="keyword">for</span> bp &lt; <span class="built_in">len</span>(nb) &#123;</span><br><span class="line"><span class="built_in">copy</span>(nb[bp:], nb[:bp])</span><br><span class="line">bp *= <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> nb</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面两个函数的实现非常相似，除了类型不同 <code>strings</code> 包在处理完数据发生了一次类型转换，使用 <code>bytes</code> 只有一次内存分配，而 <code>strings</code> 是两次。</p><p>我们可以借助 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#ToString">exbytes.ToString</a> 函数把 <code>bytes.Repeat</code> 的返回没有任何成本的转换会我们需要的字符串，如果我们输入也是一个字符串的话，还可以借助 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings#UnsafeToBytes">exstrings.UnsafeToBytes</a> 来转换输入的数据类型。</p><p>例如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s := exbytes.ToString(bytes.Repeat(exstrings.UnsafeToBytes(<span class="string">&quot;x&quot;</span>), <span class="number">10</span>))</span><br></pre></td></tr></table></figure><p>不过这样写有点太麻烦了，实际上 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exstrings">exstrings</a> 包里面正在修改 <code>strings</code> 里面一些类似函数的问题，所有的实现基本和标准库一致，只是把其中类型转换的部分用 <a href="https://godoc.org/github.com/thinkeridea/go-extend/exbytes#ToString">exbytes.ToString</a> 优化了一下，可以提升性能，也能提升开发效率。</p><p><a href="https://github.com/thinkeridea/go-extend/blob/master/exstrings/unsafe.go#L35">exstrings.UnsafeRepeat</a> 函数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">UnsafeRepeat</span><span class="params">(s <span class="keyword">string</span>, count <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="comment">// Since we cannot return an error on overflow,</span></span><br><span class="line"><span class="comment">// we should panic if the repeat will generate</span></span><br><span class="line"><span class="comment">// an overflow.</span></span><br><span class="line"><span class="comment">// See Issue golang.org/issue/16237</span></span><br><span class="line"><span class="keyword">if</span> count &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;strings: negative Repeat count&quot;</span>)</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> count &gt; <span class="number">0</span> &amp;&amp; <span class="built_in">len</span>(s)*count/count != <span class="built_in">len</span>(s) &#123;</span><br><span class="line"><span class="built_in">panic</span>(<span class="string">&quot;strings: Repeat count causes overflow&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="built_in">len</span>(s)*count)</span><br><span class="line">bp := <span class="built_in">copy</span>(b, s)</span><br><span class="line"><span class="keyword">for</span> bp &lt; <span class="built_in">len</span>(b) &#123;</span><br><span class="line"><span class="built_in">copy</span>(b[bp:], b[:bp])</span><br><span class="line">bp *= <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> exbytes.ToString(b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果用上面的函数只需要下面这样写就可以了：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s:=exstrings.UnsafeRepeat(<span class="string">&quot;x&quot;</span>, <span class="number">10</span>)</span><br></pre></td></tr></table></figure><p><a href="https://github.com/thinkeridea/go-extend/">go-extend</a> 里面还收录了很多实用的方法，大家也可以多关注。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>千万不要为了使用 <code>[]byte</code> 来优化 <code>string</code> 传递，类型转换成本很高，且 <code>slice</code> 本身也比 <code>string</code> 更大一些。</li><li>程序中是使用 <code>string</code> 还是 <code>[]byte</code> 需要根据数据来源和处理数据的函数来决定，一定要减少类型转换。</li><li>关于使用 <code>strings</code> 还是 <code>bytes</code> 包的问题，主要关注点是数据原始类型以及想获得的数据类型来选择。</li><li>减少使用字符串指针来优化字符串，这会增加 <code>GC</code> 的开销，具体可以参考 <a href="https://blog.gopheracademy.com/advent-2018/avoid-gc-overhead-large-heaps/">大堆中避免大量的GC开销</a> 一文。</li></ul>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt; 本文原标题为 《string 也是引用类型》，经过 &lt;strong&gt;郝林&lt;/strong&gt; 大佬指点原标题存在诱导性，这里解释一下 “引用类型” 有两个特征：1、多个变量引用一块内存数据，不创建变量的副本，2、修改任意变量的数据，其它变量可见。显然字符串只满足了 “引用类型” 的第一个特点，不能满足第二个特点，顾不能说字符串是引用类型，感谢大佬指正。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;初学 &lt;code&gt;Go&lt;/code&gt; 语言的朋友总会在传 &lt;code&gt;[]byte&lt;/code&gt; 和 &lt;code&gt;string&lt;/code&gt; 之间有着很多纠结，实际上是没有了解 &lt;code&gt;string&lt;/code&gt; 与 &lt;code&gt;slice&lt;/code&gt; 的本质，而且读了一些程序源码，也发现很多与之相关的问题，下面类似的代码估计很多初学者都写过，也充分说明了作者当时内心的纠结：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;17&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;package&lt;/span&gt; main&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;bytes&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;xx&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(s []&lt;span class=&quot;keyword&quot;&gt;byte&lt;/span&gt;)&lt;/span&gt; []&lt;span class=&quot;title&quot;&gt;byte&lt;/span&gt;&lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    ....&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; s&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    s := &lt;span class=&quot;string&quot;&gt;&amp;quot;xxx&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    s = &lt;span class=&quot;keyword&quot;&gt;string&lt;/span&gt;(xx([]&lt;span class=&quot;keyword&quot;&gt;byte&lt;/span&gt;(s)))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    s = &lt;span class=&quot;keyword&quot;&gt;string&lt;/span&gt;(bytes.Replace([]&lt;span class=&quot;keyword&quot;&gt;byte&lt;/span&gt;(s), []&lt;span class=&quot;keyword&quot;&gt;byte&lt;/span&gt;(&lt;span class=&quot;string&quot;&gt;&amp;quot;x&amp;quot;&lt;/span&gt;), []&lt;span class=&quot;keyword&quot;&gt;byte&lt;/span&gt;(&lt;span class=&quot;string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;), &lt;span class=&quot;number&quot;&gt;-1&lt;/span&gt;))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;虽然这样的代码并不是来自真实的项目，但是确实有人这样设计，单从设计上看就很糟糕了，这样设计的原因很多人说：“&lt;code&gt;slice&lt;/code&gt; 是引用类型，传递引用类型效率高呀”，主要原因不了解两者的本质。&lt;/p&gt;
&lt;p&gt;上面这个例子如果觉得有点基础和可爱，下面这个例子貌似并不那么容易说明其存在的问题了吧。&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;package&lt;/span&gt; main&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;xx&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(s *&lt;span class=&quot;keyword&quot;&gt;string&lt;/span&gt;)&lt;/span&gt; *&lt;span class=&quot;title&quot;&gt;string&lt;/span&gt;&lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    ....&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; s&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;()&lt;/span&gt;&lt;/span&gt;&amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    s := &lt;span class=&quot;string&quot;&gt;&amp;quot;xx&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    s = *xx(&amp;amp;s)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    ss :=[]*&lt;span class=&quot;keyword&quot;&gt;string&lt;/span&gt;&amp;#123;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    ss = &lt;span class=&quot;built_in&quot;&gt;append&lt;/span&gt;(ss, &amp;amp;s)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;指针效率高，我就用指针多好，可以减少内存分配呀，设计函数都接收指针变量，程序性能会有很大提升，在实际的项目中这种例子也不少见，我想通过这篇文档来帮助初学者走出误区，减少适得其反的优化技巧。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="bytes" scheme="https://blog.thinkeridea.com/tags/bytes/"/>
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="unsafe" scheme="https://blog.thinkeridea.com/tags/unsafe/"/>
    
    <category term="string" scheme="https://blog.thinkeridea.com/tags/string/"/>
    
  </entry>
  
  <entry>
    <title>【Go】strings.Replace 与 bytes.Replace 调优</title>
    <link href="https://blog.thinkeridea.com/201902/go/replcae_you_hua.html"/>
    <id>https://blog.thinkeridea.com/201902/go/replcae_you_hua.html</id>
    <published>2019-02-01T22:34:57.000Z</published>
    <updated>2019-02-24T12:54:05.247Z</updated>
    
    <content type="html"><![CDATA[<p>标准库中函数大多数情况下更通用，性能并非最好的，还是不能过于迷信标准库，最近又有了新发现，<code>strings.Replace</code> 这个函数自身的效率已经很好了，但是在特定情况下效率并不是最好的，分享一下我如何优化的吧。</p><p>我的服务中有部分代码使用 <code>strings.Replace</code> 把一个固定的字符串删除或者替换成另一个字符串，它们有几个特点：</p><ul><li>旧的字符串大于或等于新字符串 <code>(len(old) &gt;= len(new)</code></li><li>源字符串的生命周期很短，替换后就不再使用替换前的字符串</li><li>它们都比较大，往往超过 2k~4k</li></ul><p>本博文中使用函数均在 <a href="https://github.com/thinkeridea/go-extend"><strong>go-extend</strong></a> 中，优化后的函数在 <a href="https://github.com/thinkeridea/go-extend/blob/master/exbytes/bytes.go">exbytes.Replace</a> 中。</p><a id="more"></a><h2 id="发现问题"><a href="#发现问题" class="headerlink" title="发现问题"></a>发现问题</h2><p>近期使用 <code>pprof</code> 分析内存分配情况，发现 <code>strings.Replace</code> 排在第二，占 <code>7.54%</code>, 分析结果如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">go tool pprof allocs</span><br><span class="line">File: xxx</span><br><span class="line">Type: alloc_space</span><br><span class="line">Time: Feb 1, 2019 at 9:53pm (CST)</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands, &quot;o&quot; for options)</span><br><span class="line">(pprof) top</span><br><span class="line">Showing nodes accounting for 617.29GB, 48.86% of 1263.51GB total</span><br><span class="line">Dropped 778 nodes (cum &lt;= 6.32GB)</span><br><span class="line">Showing top 10 nodes out of 157</span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">  138.28GB 10.94% 10.94%   138.28GB 10.94%  logrus.(*Entry).WithFields</span><br><span class="line">   95.27GB  7.54% 18.48%    95.27GB  7.54%  strings.Replace</span><br><span class="line">   67.05GB  5.31% 23.79%   185.09GB 14.65%  v3.(*v3Adapter).parseEncrypt</span><br><span class="line">   57.01GB  4.51% 28.30%    57.01GB  4.51%  bufio.NewWriterSize</span><br><span class="line">   56.63GB  4.48% 32.78%    56.63GB  4.48%  bufio.NewReaderSize</span><br><span class="line">   56.11GB  4.44% 37.23%    56.11GB  4.44%  net/url.unescape</span><br><span class="line">   39.75GB  3.15% 40.37%    39.75GB  3.15%  regexp.(*bitState).reset</span><br><span class="line">   36.11GB  2.86% 43.23%    38.05GB  3.01%  des3_and_base64.(*des3AndBase64).des3Decrypt</span><br><span class="line">   36.01GB  2.85% 46.08%    36.01GB  2.85%  des3_and_base64.(*des3AndBase64).base64Decode</span><br><span class="line">   35.08GB  2.78% 48.86%    35.08GB  2.78%  math/big.nat.make</span><br></pre></td></tr></table></figure><p>标准库中最常用的函数，居然……，不可忍必须优化，先使用 <code>list strings.Replace</code> 看一下源码什么地方分配的内存。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">(pprof) list strings.Replace</span><br><span class="line">Total: 1.23TB</span><br><span class="line">ROUTINE ======================== strings.Replace in /usr/local/go/src/strings/strings.go</span><br><span class="line">   95.27GB    95.27GB (flat, cum)  7.54% of Total</span><br><span class="line">         .          .    858:&#125; else if n &lt; 0 || m &lt; n &#123;</span><br><span class="line">         .          .    859:n = m</span><br><span class="line">         .          .    860:&#125;</span><br><span class="line">         .          .    861:</span><br><span class="line">         .          .    862:// Apply replacements to buffer.</span><br><span class="line">   47.46GB    47.46GB    863:t := make([]byte, len(s)+n*(len(new)-len(old)))</span><br><span class="line">         .          .    864:w := 0</span><br><span class="line">         .          .    865:start := 0</span><br><span class="line">         .          .    866:for i := 0; i &lt; n; i++ &#123;</span><br><span class="line">         .          .    867:j := start</span><br><span class="line">         .          .    868:if len(old) == 0 &#123;</span><br><span class="line">         .          .    869:if i &gt; 0 &#123;</span><br><span class="line">         .          .    870:_, wid := utf8.DecodeRuneInString(s[start:])</span><br><span class="line">         .          .    871:j += wid</span><br><span class="line">         .          .    872:&#125;</span><br><span class="line">         .          .    873:&#125; else &#123;</span><br><span class="line">         .          .    874:j += Index(s[start:], old)</span><br><span class="line">         .          .    875:&#125;</span><br><span class="line">         .          .    876:w += copy(t[w:], s[start:j])</span><br><span class="line">         .          .    877:w += copy(t[w:], new)</span><br><span class="line">         .          .    878:start = j + len(old)</span><br><span class="line">         .          .    879:&#125;</span><br><span class="line">         .          .    880:w += copy(t[w:], s[start:])</span><br><span class="line">   47.81GB    47.81GB    881:return string(t[0:w])</span><br><span class="line">         .          .    882:&#125;</span><br></pre></td></tr></table></figure><p>从源码发现首先创建了一个 <code>buffer</code> 来起到缓冲的效果，一次分配足够的内存，这个在之前 <a href="https://blog.thinkeridea.com/201901/go/slice_de_yi_xie_shi_yong_ji_qiao.html">【Go】slice的一些使用技巧</a> 里面有讲到，另外一个是 <code>string(t[0:w])</code> 类型转换带来的内存拷贝，<code>buffer</code> 能够理解，但是类型转换这个不能忍，就像凭空多出来的一个数拷贝。</p><p>既然类型转换这里有点浪费空间，有没有办法可以零成本转换呢，那就使用 <a href="https://github.com/thinkeridea/go-extend/blob/master/exbytes/convert.go">go-extend</a> 这个包里面的 <code>exbytes.ToString</code> 方法把 <code>[]byte</code> 转换成 <code>string</code>，这个函数可以零分配转换 <code>[]byte</code> 到 <code>string</code>。 <code>t</code> 是一个临时变量，可以安全的被引用不用担心，一个小技巧节省一倍的内存分配，但是这样真的就够了吗？</p><p>我记得 <code>bytes</code> 标准库里面也有一个 <code>bytes.Replace</code> 方法，如果直接使用这种方法呢就不用重写一个 <code>strings.Replace</code>了，使用 <a href="https://github.com/thinkeridea/go-extend/">go-extend</a> 里面的两个魔术方法可以一行代码搞定上面的优化效果 <code>s = exbytes.ToString(bytes.Replace(exstrings.UnsafeToBytes(s), []byte&#123;&#39; &#39;&#125;, []byte&#123;&#39;&#39;&#125;, -1))</code>， 虽然是一行代码搞定的，但是有点长，<code>exstrings.UnsafeToBytes</code> 方法可以极小的代价把 <code>string</code> 转成 <code>bytes</code>， 但是 <code>s</code> 不能是标量或常量字符串，必须是运行时产生的字符串否者可能导致程序奔溃。</p><p>这样确实减少了一倍的内存分配，即使只有 <code>47.46GB</code> 的分配也足以排到前十了，不满意这个结果，分析代码看看能不能更进一步减少内存分配吧。</p><h2 id="分析代码"><a href="#分析代码" class="headerlink" title="分析代码"></a>分析代码</h2><p>使用火焰图看看究竟什么函数在调用 <code>strings.Replace</code> 呢：</p><p><img src="/assets/image/201902/0201.jpg" alt="image-20190202184040233"></p><p>这里主要是两个方法在使用，当然我记得还有几个地方有使用，看来不在火焰图中应该影响比较低 ，看一下代码吧（简化的代码不一定完全合理）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 第一部分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(v2 *v2Adapter)</span> <span class="title">parse</span><span class="params">(s <span class="keyword">string</span>)</span> <span class="params">(*AdRequest, error)</span></span> &#123;</span><br><span class="line">s = strings.Replace(s, <span class="string">&quot; &quot;</span>, <span class="string">&quot;&quot;</span>, <span class="number">-1</span>)</span><br><span class="line">requestJSON, err := v2.paramCrypto.Decrypt([]<span class="keyword">byte</span>(s))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">request := v2.getDefaultAdRequest()</span><br><span class="line"><span class="keyword">if</span> err := request.UnmarshalJSON(requestJSON); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> request, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二部分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(v3 *v3Adapter)</span> <span class="title">parseEncrypt</span><span class="params">(s []<span class="keyword">byte</span>)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">    ss := strings.Replace(<span class="keyword">string</span>(s), <span class="string">&quot; &quot;</span>, <span class="string">&quot;&quot;</span>, <span class="number">-1</span>)</span><br><span class="line">    requestJSON, err := v3.paramCrypto.Decrypt([]<span class="keyword">byte</span>(ss))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> requestJSON, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 通过搜索找到的第三部分</span></span><br><span class="line"><span class="keyword">type</span> LogItems []<span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">LogItemsToBytes</span><span class="params">(items []<span class="keyword">string</span>, sep, newline <span class="keyword">string</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> items &#123;</span><br><span class="line">items[i] = strings.Replace(items[i], sep, <span class="string">&quot; &quot;</span>, <span class="number">-1</span>)</span><br><span class="line">&#125;</span><br><span class="line">str := strings.Replace(strings.Join(items, sep), newline, <span class="string">&quot; &quot;</span>, <span class="number">-1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> []<span class="keyword">byte</span>(str + newline)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过分析我们发现前两个主要是为了删除一个字符串，第三个是为了把一个字符串替换为另一个字符串，并且源数据的生命周期很短暂，在执行替换之后就不再使用了，能不能原地替换字符串呢，原地替换的就会变成零分配了，尝试一下吧。</p><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><p>先写一个函数简单实现原地替换，输入的 <code>len(old) &lt; len(new)</code> 就直接调用 <code>bytes.Replace</code> 来实现就好了 。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Replace</span><span class="params">(s, old, <span class="built_in">new</span> []<span class="keyword">byte</span>, n <span class="keyword">int</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> n == <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(old) &lt; <span class="built_in">len</span>(<span class="built_in">new</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.Replace(s, old, <span class="built_in">new</span>, n)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> n &lt; <span class="number">0</span> &#123;</span><br><span class="line">n = <span class="built_in">len</span>(s)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> wid, i, j <span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> i, j = <span class="number">0</span>, <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s) &amp;&amp; j &lt; n; j++ &#123;</span><br><span class="line">wid = bytes.Index(s[i:], old)</span><br><span class="line"><span class="keyword">if</span> wid &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">i += wid</span><br><span class="line">i += <span class="built_in">copy</span>(s[i:], <span class="built_in">new</span>)</span><br><span class="line">s = <span class="built_in">append</span>(s[:i], s[i+<span class="built_in">len</span>(old)-<span class="built_in">len</span>(<span class="built_in">new</span>):]...)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写个性能测试看一下效果：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go <span class="built_in">test</span> -bench=<span class="string">&quot;.&quot;</span> -run=nil -benchmem</span></span><br><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exbytes/benchmark</span><br><span class="line">BenchmarkReplace-8                  500000      3139 ns/op     416 B/op       1 allocs/op</span><br><span class="line">BenchmarkBytesReplace-8            1000000      2032 ns/op     736 B/op       2 allocs/op</span><br></pre></td></tr></table></figure><p>使用这个新的函数和 <code>bytes.Replace</code> 对比，内存分配是少了，但是性能却下降了那么多，崩溃…. 啥情况呢，对比 <code>bytes.Replace</code> 的源码发现我这个代码里面 <code>s = append(s[:i], s[i+len(old)-len(new):]...)</code> 每次都会移动剩余的数据导致性能差异很大，可以使用 <code> go test -bench=&quot;.&quot; -run=nil -benchmem  -cpuprofile cpu.out -memprofile mem.out</code> 的方式来生成 <code>pprof</code> 数据，然后分析具体有问题的地方。</p><p>找到问题就好了，移动 <code>wid</code> 之前的数据，这样每次移动就很少了，和 <code>bytes.Replace</code> 的原理类似。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Replace</span><span class="params">(s, old, <span class="built_in">new</span> []<span class="keyword">byte</span>, n <span class="keyword">int</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> n == <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(old) &lt; <span class="built_in">len</span>(<span class="built_in">new</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.Replace(s, old, <span class="built_in">new</span>, n)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> n &lt; <span class="number">0</span> &#123;</span><br><span class="line">n = <span class="built_in">len</span>(s)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> wid, i, j, w <span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> i, j = <span class="number">0</span>, <span class="number">0</span>; i &lt; <span class="built_in">len</span>(s) &amp;&amp; j &lt; n; j++ &#123;</span><br><span class="line">wid = bytes.Index(s[i:], old)</span><br><span class="line"><span class="keyword">if</span> wid &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">w += <span class="built_in">copy</span>(s[w:], s[i:i+wid])</span><br><span class="line">w += <span class="built_in">copy</span>(s[w:], <span class="built_in">new</span>)</span><br><span class="line">i += wid + <span class="built_in">len</span>(old)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">w += <span class="built_in">copy</span>(s[w:], s[i:])</span><br><span class="line"><span class="keyword">return</span> s[<span class="number">0</span>:w]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在运行一下性能测试吧：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go <span class="built_in">test</span> -bench=<span class="string">&quot;.&quot;</span> -run=nil -benchmem</span></span><br><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exbytes/benchmark</span><br><span class="line">BenchmarkReplace-8                 1000000      2149 ns/op     416 B/op       1 allocs/op</span><br><span class="line">BenchmarkBytesReplace-8            1000000      2231 ns/op     736 B/op       2 allocs/op</span><br></pre></td></tr></table></figure><p>运行性能差不多，而且更好了，内存分配也减少，不是说是零分配吗，为啥有一次分配呢？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> replaces <span class="keyword">string</span></span><br><span class="line"><span class="keyword">var</span> replaceb []<span class="keyword">byte</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">replaces = strings.Repeat(<span class="string">&quot;A BC&quot;</span>, <span class="number">100</span>)</span><br><span class="line">replaceb = bytes.Repeat([]<span class="keyword">byte</span>(<span class="string">&quot;A BC&quot;</span>), <span class="number">100</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkReplace</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">exbytes.Replace([]<span class="keyword">byte</span>(replaces), []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), []<span class="keyword">byte</span>(<span class="string">&quot;&quot;</span>), <span class="number">-1</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkBytesReplace</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">bytes.Replace([]<span class="keyword">byte</span>(replaces), []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), []<span class="keyword">byte</span>(<span class="string">&quot;&quot;</span>), <span class="number">-1</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到使用了 <code>[]byte(replaces)</code> 做了一次类型转换，因为优化的这个函数是原地替换，执行过一次之后后面就发现不用替换了，所以为了公平公正两个方法每次都转换一个类型产生一个新的内存地址，所以实际优化后是没有内存分配了。</p><p>之前说写一个优化 <code>strings.Replace</code> 函数，减少一次内存分配，这里也写一个这样函数，然后增加两个性能测试函数，对比一下效率 <a href="https://github.com/thinkeridea/go-extend/blob/master/exbytes/benchmark/bytes_test.go">性能测试代码</a>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go <span class="built_in">test</span> -bench=<span class="string">&quot;.&quot;</span> -run=nil -benchmem</span></span><br><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/go-extend/exbytes/benchmark</span><br><span class="line">BenchmarkReplace-8                 1000000      2149 ns/op     416 B/op       1 allocs/op</span><br><span class="line">BenchmarkBytesReplace-8            1000000      2231 ns/op     736 B/op       2 allocs/op</span><br><span class="line">BenchmarkStringsReplace-8          1000000      2260 ns/op    1056 B/op       3 allocs/op</span><br><span class="line">BenchmarkUnsafeStringsReplace-8    1000000      2522 ns/op     736 B/op       2 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com/thinkeridea/go-extend/exbytes/benchmark10.260s</span><br></pre></td></tr></table></figure><p>运行效率上都相当，优化之后的 <code>UnsafeStringsReplace</code> 函数减少了一次内存分配只有一次，和 <code>bytes.Replace</code> 相当。</p><h2 id="修改代码"><a href="#修改代码" class="headerlink" title="修改代码"></a>修改代码</h2><p>有了优化版的 <code>Replace</code> 函数就替换到项目中吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 第一部分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(v2 *v2Adapter)</span> <span class="title">parse</span><span class="params">(s <span class="keyword">string</span>)</span> <span class="params">(*AdRequest, error)</span></span> &#123;</span><br><span class="line">b := exbytes.Replace(exstrings.UnsafeToBytes(s), []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), []<span class="keyword">byte</span>(<span class="string">&quot;&quot;</span>), <span class="number">-1</span>)</span><br><span class="line">requestJSON, err := v2.paramCrypto.Decrypt(b)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line">request := v2.getDefaultAdRequest()</span><br><span class="line"><span class="keyword">if</span> err := request.UnmarshalJSON(requestJSON); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> request, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第二部分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(v3 *v3Adapter)</span> <span class="title">parseEncrypt</span><span class="params">(s []<span class="keyword">byte</span>)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">s = exbytes.Replace(s, []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), []<span class="keyword">byte</span>(<span class="string">&quot;&quot;</span>), <span class="number">-1</span>)</span><br><span class="line">requestJSON, err := v3.paramCrypto.Decrypt(s)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> requestJSON, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 第三部分</span></span><br><span class="line"><span class="keyword">type</span> LogItems []<span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">LogItemsToBytes</span><span class="params">(items []<span class="keyword">string</span>, sep, newline <span class="keyword">string</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> items &#123;</span><br><span class="line">items[i] = exbytes.ToString(exbytes.Replace(exstrings.UnsafeToBytes(items[i]), []<span class="keyword">byte</span>(sep), []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), <span class="number">-1</span>))</span><br><span class="line">&#125;</span><br><span class="line">b := exbytes.Replace(exstrings.UnsafeToBytes(strings.Join(items, sep)), []<span class="keyword">byte</span>(newline), []<span class="keyword">byte</span>(<span class="string">&quot; &quot;</span>), <span class="number">-1</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">append</span>(b, newline...)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="上线后性能分析"><a href="#上线后性能分析" class="headerlink" title="上线后性能分析"></a>上线后性能分析</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go tool pprof allocs2</span></span><br><span class="line">File: xx</span><br><span class="line">Type: alloc_space</span><br><span class="line">Time: Feb 2, 2019 at 5:33pm (CST)</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands, &quot;o&quot; for options)</span><br><span class="line">(pprof) top exbytes.Replace</span><br><span class="line">Focus expression matched no samples</span><br><span class="line">Active filters:</span><br><span class="line">   focus=exbytes.Replace</span><br><span class="line">Showing nodes accounting for 0, 0% of 864.21GB total</span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">(pprof)</span><br></pre></td></tr></table></figure><p>居然在 <code>allocs</code> 上居然找不到了，确实是零分配。</p><p>优化前 <code>profile</code> ：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go tool pprof profile</span></span><br><span class="line">File: xx</span><br><span class="line">Type: cpu</span><br><span class="line">Time: Feb 1, 2019 at 9:54pm (CST)</span><br><span class="line">Duration: 30.08s, Total samples = 12.23s (40.65%)</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands, &quot;o&quot; for options)</span><br><span class="line">(pprof) top strings.Replace</span><br><span class="line">Active filters:</span><br><span class="line">   focus=strings.Replace</span><br><span class="line">Showing nodes accounting for 0.08s, 0.65% of 12.23s total</span><br><span class="line">Showing top 10 nodes out of 27</span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">     0.03s  0.25%  0.25%      0.08s  0.65%  strings.Replace</span><br><span class="line">     0.02s  0.16%  0.41%      0.02s  0.16%  countbody</span><br><span class="line">     0.01s 0.082%  0.49%      0.01s 0.082%  indexbytebody</span><br><span class="line">     0.01s 0.082%  0.57%      0.01s 0.082%  memeqbody</span><br><span class="line">     0.01s 0.082%  0.65%      0.01s 0.082%  runtime.scanobject</span><br></pre></td></tr></table></figure><p>优化后 <code>profile</code> ：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">$</span><span class="bash"> go tool pprof profile2</span></span><br><span class="line">File: xx</span><br><span class="line">Type: cpu</span><br><span class="line">Time: Feb 2, 2019 at 5:33pm (CST)</span><br><span class="line">Duration: 30.16s, Total samples = 14.68s (48.68%)</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands, &quot;o&quot; for options)</span><br><span class="line">(pprof) top exbytes.Replace</span><br><span class="line">Active filters:</span><br><span class="line">   focus=exbytes.Replace</span><br><span class="line">Showing nodes accounting for 0.06s, 0.41% of 14.68s total</span><br><span class="line">Showing top 10 nodes out of 18</span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">     0.03s   0.2%   0.2%      0.03s   0.2%  indexbytebody</span><br><span class="line">     0.02s  0.14%  0.34%      0.05s  0.34%  bytes.Index</span><br><span class="line">     0.01s 0.068%  0.41%      0.06s  0.41%  github.com/thinkeridea/go-extend/exbytes.Replace</span><br></pre></td></tr></table></figure><p>通过 <code>profile</code> 来分配发现性能也有一定的提升，本次 <code>strings.Replace</code> 和 <code>bytes.Replace</code> 优化圆满结束。</p><p>本博文中使用函数均在 <a href="https://github.com/thinkeridea/go-extend">go-extend</a> 中，优化后的函数在 <a href="https://github.com/thinkeridea/go-extend/blob/master/exbytes/bytes.go">exbytes.Replace</a> 中。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;标准库中函数大多数情况下更通用，性能并非最好的，还是不能过于迷信标准库，最近又有了新发现，&lt;code&gt;strings.Replace&lt;/code&gt; 这个函数自身的效率已经很好了，但是在特定情况下效率并不是最好的，分享一下我如何优化的吧。&lt;/p&gt;
&lt;p&gt;我的服务中有部分代码使用 &lt;code&gt;strings.Replace&lt;/code&gt; 把一个固定的字符串删除或者替换成另一个字符串，它们有几个特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旧的字符串大于或等于新字符串 &lt;code&gt;(len(old) &amp;gt;= len(new)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;源字符串的生命周期很短，替换后就不再使用替换前的字符串&lt;/li&gt;
&lt;li&gt;它们都比较大，往往超过 2k~4k&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本博文中使用函数均在 &lt;a href=&quot;https://github.com/thinkeridea/go-extend&quot;&gt;&lt;strong&gt;go-extend&lt;/strong&gt;&lt;/a&gt; 中，优化后的函数在 &lt;a href=&quot;https://github.com/thinkeridea/go-extend/blob/master/exbytes/bytes.go&quot;&gt;exbytes.Replace&lt;/a&gt; 中。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="bytes" scheme="https://blog.thinkeridea.com/tags/bytes/"/>
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="strings" scheme="https://blog.thinkeridea.com/tags/strings/"/>
    
    <category term="replace" scheme="https://blog.thinkeridea.com/tags/replace/"/>
    
    <category term="unsafe" scheme="https://blog.thinkeridea.com/tags/unsafe/"/>
    
  </entry>
  
  <entry>
    <title>【Go】优雅的读取http请求或响应的数据-续</title>
    <link href="https://blog.thinkeridea.com/201902/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju_2.html"/>
    <id>https://blog.thinkeridea.com/201902/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju_2.html</id>
    <published>2019-02-01T08:43:26.000Z</published>
    <updated>2019-07-07T13:13:04.851Z</updated>
    
    <content type="html"><![CDATA[<p>之前发布 <a href="https://blog.thinkeridea.com/201901/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju.html">【Go】优雅的读取http请求或响应的数据</a> 文章，网友 “wxe” 咨询：“优化前后的请求耗时变化有多大”，之前只分析了内存分配，这篇文章用单元测试的方式分析优化前后的耗时情况，<a href="https://github.com/thinkeridea/example/tree/master/iouitl_readall">本文源码</a>。</p><p>非常感谢 “wxe” 网友的提问，让我在测试过程中发现一个 <code>json</code> 序列化的问题。</p><a id="more"></a><p>之前我们优化了两个部分，<code>json</code> 与 <code>ioutil.ReadAll</code>， 先对比 <code>ioutil.ReadAll</code>, 这里测试的代码分成两个部分做对比，一部分单纯对比 <code>ioutil.ReadAll</code> 和 <code>io.Copy</code> + <code>sync.Pool</code>，另一部分增加 <code>jsoniter.Unmarshal</code> 来延迟 <code>pool.Put(buffer)</code> 的执行, <a href="https://github.com/thinkeridea/example/blob/master/iouitl_readall/io.go">源码</a>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> iouitl_readall</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"><span class="string">&quot;io/ioutil&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line">jsoniter <span class="string">&quot;github.com/json-iterator/go&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> pool = sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">4096</span>))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IoCopyAndJson</span><span class="params">(r io.Reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">buffer := pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> pool.Put(buffer)</span><br><span class="line"></span><br><span class="line">res := Do(r)</span><br><span class="line">_, err := io.Copy(buffer, res)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">m := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;&#125;</span><br><span class="line">err = jsoniter.Unmarshal(buffer.Bytes(), &amp;m)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IouitlReadAllAndJson</span><span class="params">(r io.Reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">res := Do(r)</span><br><span class="line">data, err := ioutil.ReadAll(res)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">m := <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>&#123;&#125;</span><br><span class="line">err = jsoniter.Unmarshal(data, &amp;m)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IoCopy</span><span class="params">(r io.Reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">buffer := pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> pool.Put(buffer)</span><br><span class="line"></span><br><span class="line">res := Do(r)</span><br><span class="line">_, err := io.Copy(buffer, res)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IouitlReadAll</span><span class="params">(r io.Reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">res := Do(r)</span><br><span class="line">data, err := ioutil.ReadAll(res)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">_ = data</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试代码如下<a href="https://github.com/thinkeridea/example/blob/master/iouitl_readall/io_test.go">源码</a>:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> iouitl_readall</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> data = bytes.Repeat([]<span class="keyword">byte</span>(<span class="string">&quot;ABCD&quot;</span>), <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkIouitlReadAll</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := IouitlReadAll(bytes.NewReader(data))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkIoCopy</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := IoCopy(bytes.NewReader(data))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkIouitlReadAllAndJson</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := IouitlReadAllAndJson(bytes.NewReader(data))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkIoCopyAndJson</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := IoCopyAndJson(bytes.NewReader(data))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试结果如下: </p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/example/iouitl_readall</span><br><span class="line">BenchmarkIouitlReadAll-8            500000      2752 ns/op   14496 B/op       6 allocs/op</span><br><span class="line">BenchmarkIoCopy-8                 20000000        65.2 ns/op      48 B/op       1 allocs/op</span><br><span class="line">BenchmarkIouitlReadAllAndJson-8     100000     20022 ns/op   46542 B/op     616 allocs/op</span><br><span class="line">BenchmarkIoCopyAndJson-8            100000     17615 ns/op   32102 B/op     611 allocs/op</span><br></pre></td></tr></table></figure><p>结论：</p><p>可以发现 <code>IoCopy</code> 方法是 <code>IouitlReadAll</code> 方法效率的 40 倍，内存分配也很少，而 <code>IoCopyAndJson</code> 和 <code>IouitlReadAllAndJson</code> 的效率差异极小仅有 <code>2407ns</code>，大约是 1.13倍，不过内存分配还是少了很多的，为什么会这样呢，这就是 <code>sync.Pool</code> 的导致的，<code>sync.Pool</code> 每次获取使用时间越短，命中率就越高，就可以减少创建新的缓存，这样效率就会大大提高，而 <code>jsoniter.Unmarshal</code> 很耗时，就导致 <code>sync.Pool</code> 的命中率降低了，所以性能下降极其明显.</p><p>使用 <code>io.Copy</code> + <code>sync.Pool</code> 表面上执行效率不会有很大提升，但是会大幅度减少内存分配，从而可以减少 <code>GC</code> 的负担，在单元测试中我们并没有考虑 <code>GC</code> 的问题，而 <code>GC</code> 能带来的性能提升会更有优势。</p><p>在看一下 <code>json</code> 使用 <code>sync.Pool</code> 的效果吧 <a href="https://github.com/thinkeridea/example/blob/master/iouitl_readall/json.go">源码</a></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> iouitl_readall</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;encoding/json&quot;</span></span><br><span class="line"></span><br><span class="line">jsoniter <span class="string">&quot;github.com/json-iterator/go&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Json</span><span class="params">(r <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">data, err := json.Marshal(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">_ = data</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">JsonPool</span><span class="params">(r <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">buffer := pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> pool.Put(buffer)</span><br><span class="line"></span><br><span class="line">e := json.NewEncoder(buffer)</span><br><span class="line">err := e.Encode(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">JsonIter</span><span class="params">(r <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">data, err := jsoniter.Marshal(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">_ = data</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">JsonIterPool</span><span class="params">(r <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">buffer := pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> pool.Put(buffer)</span><br><span class="line"></span><br><span class="line">e := jsoniter.NewEncoder(buffer)</span><br><span class="line">err := e.Encode(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>性能测试代码<a href="https://github.com/thinkeridea/example/blob/master/iouitl_readall/json_test.go">源码</a>: </p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> iouitl_readall</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;strconv&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> request <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">request = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">string</span>, <span class="number">100</span>)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">100</span>; i++ &#123;</span><br><span class="line">request[<span class="string">&quot;X&quot;</span>+strconv.Itoa(i)] = strings.Repeat(<span class="string">&quot;A&quot;</span>, i/<span class="number">2</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkJson</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := Json(request)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkJsonIter</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := JsonIter(request)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkJsonPool</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := JsonPool(request)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkJsonIterPool</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">b.RunParallel(<span class="function"><span class="keyword">func</span><span class="params">(pb *testing.PB)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> pb.Next() &#123;</span><br><span class="line">err := JsonIterPool(request)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">b.Error(err.Error())</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试结果如下:</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com/thinkeridea/example/iouitl_readall</span><br><span class="line">BenchmarkJson-8                     100000     13297 ns/op   13669 B/op     207 allocs/op</span><br><span class="line">BenchmarkJsonPool-8                 100000     13310 ns/op   10218 B/op     206 allocs/op</span><br><span class="line">BenchmarkJsonIter-8                 500000      2948 ns/op    3594 B/op       4 allocs/op</span><br><span class="line">BenchmarkJsonIterPool-8             200000      6126 ns/op    6040 B/op     144 allocs/op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com/thinkeridea/example/iouitl_readall12.716s</span><br></pre></td></tr></table></figure><p>这里使用了两个 <code>json</code> 包， 一个是标准库的，一个是 <code>jsoniter</code> (也是社区反馈效率最高的)，对比两个包使用 <code>sync.Pool</code> 和不使用之间的差异，发现标准库 <code>json</code> 包使用后内存有少量减少，但是运行效率稍微下降了，差异不是很大，<code>jsoniter</code> 包差异之所谓非常明显，发现使用 <code>sync.Pool</code> 之后不仅内存分配更多了，执行效率也大幅度下降，差了将近3倍有余。</p><p>是不是很奔溃，这是啥情况 <code>jsoniter</code> 本身就使用了 <code>sync.Pool</code> 作缓冲，我们使用 <code>jsoniter.NewEncoder(buffer)</code> 创建一个序列化实例，但是其内部并没有直接使用 <code>io.Writer</code> 而是先使用缓冲序列化数据，之后写入 <code>io.Writer</code>, 具体代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Flush writes any buffered data to the underlying io.Writer.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(stream *Stream)</span> <span class="title">Flush</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> stream.out == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> stream.Error != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> stream.Error</span><br><span class="line">&#125;</span><br><span class="line">n, err := stream.out.Write(stream.buf)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">if</span> stream.Error == <span class="literal">nil</span> &#123;</span><br><span class="line">stream.Error = err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line">stream.buf = stream.buf[n:]</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样一来我们使用 <code>buffer</code> 做 <code>json</code> 序列化优化效果就大打折扣，甚至适得其反了。</p><p>再次感谢 “wxe” 网友的提问，这里没有使用实际的应用场景做性能测试，主要发现在性能测试中使用 <code>http</code> 服务会导致 <code>connect: can&#39;t assign requested address</code> 问题，所以测试用使用了函数模拟，如果有朋友有更好的测试方法欢迎一起交流。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;之前发布 &lt;a href=&quot;https://blog.thinkeridea.com/201901/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju.html&quot;&gt;【Go】优雅的读取http请求或响应的数据&lt;/a&gt; 文章，网友 “wxe” 咨询：“优化前后的请求耗时变化有多大”，之前只分析了内存分配，这篇文章用单元测试的方式分析优化前后的耗时情况，&lt;a href=&quot;https://github.com/thinkeridea/example/tree/master/iouitl_readall&quot;&gt;本文源码&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;非常感谢 “wxe” 网友的提问，让我在测试过程中发现一个 &lt;code&gt;json&lt;/code&gt; 序列化的问题。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="net.http" scheme="https://blog.thinkeridea.com/tags/net-http/"/>
    
    <category term="http" scheme="https://blog.thinkeridea.com/tags/http/"/>
    
    <category term="pool" scheme="https://blog.thinkeridea.com/tags/pool/"/>
    
    <category term="bytes" scheme="https://blog.thinkeridea.com/tags/bytes/"/>
    
    <category term="buffer" scheme="https://blog.thinkeridea.com/tags/buffer/"/>
    
    <category term="makeSlice" scheme="https://blog.thinkeridea.com/tags/makeslice/"/>
    
    <category term="ioutil" scheme="https://blog.thinkeridea.com/tags/ioutil/"/>
    
  </entry>
  
  <entry>
    <title>【Go】优雅的读取http请求或响应的数据</title>
    <link href="https://blog.thinkeridea.com/201901/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju.html"/>
    <id>https://blog.thinkeridea.com/201901/go/you_ya_de_du_qu_http_qing_qiu_huo_xiang_ying_de_shu_ju.html</id>
    <published>2019-01-26T08:43:26.000Z</published>
    <updated>2019-02-24T12:53:52.573Z</updated>
    
    <content type="html"><![CDATA[<p>从 <code>http.Request.Body</code> 或 <code>http.Response.Body</code> 中读取数据方法或许很多，标准库中大多数使用 <code>ioutil.ReadAll</code> 方法一次读取所有数据，如果是 <code>json</code> 格式的数据还可以使用 <code>json.NewDecoder</code> 从 <code>io.Reader</code> 创建一个解析器，假使使用 <code>pprof</code> 来分析程序总是会发现 <code>bytes.makeSlice</code> 分配了大量内存，且总是排行第一，今天就这个问题来说一下如何高效优雅的读取 <code>http</code> 中的数据。</p><a id="more"></a><h2 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h2><p>我们有许多 <code>api</code> 服务，全部采用 <code>json</code> 数据格式，请求体就是整个 <code>json</code> 字符串，当一个请求到服务端会经过一些业务处理，然后再请求后面更多的服务，所有的服务之间都用 <code>http</code> 协议来通信(啊， 为啥不用 <code>RPC</code>，因为所有的服务都会对第三方开放，<code>http</code> + <code>json</code> 更好对接)，大多数请求数据大小在 1K<del>4K，响应的数据在 1K</del>8K，早期所有的服务都使用 <code>ioutil.ReadAll</code> 来读取数据，随着流量增加使用 <code>pprof</code> 来分析发现 <code>bytes.makeSlice</code> 总是排在第一，并且占用了整个程序 <code>1/10</code> 的内存分配，我决定针对这个问题进行优化，下面是整个优化过程的记录。</p><h2 id="pprof-分析"><a href="#pprof-分析" class="headerlink" title="pprof 分析"></a>pprof 分析</h2><p>这里使用 <a href="https://github.com/thinkeridea/go-extend/blob/master/exnet/exhttp/expprof/pprof.go">https://github.com/thinkeridea/go-extend/blob/master/exnet/exhttp/expprof/pprof.go</a> 中的 <code>API</code> 来实现生产环境的 <code>/debug/pprof</code> 监测接口，没有使用标准库的 <code>net/http/pprof</code> 包因为会自动注册路由，且长期开放 <code>API</code>，这个包可以设定 <code>API</code> 是否开放，并在规定时间后自动关闭接口，避免存在工具嗅探。</p><p>服务部署上线稳定后(大约过了一天半)，通过 <code>curl</code> 下载 <code>allocs</code> 数据，然后使用下面的命令查看分析。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$</span> go tool pprof allocs</span><br><span class="line">File: xxx</span><br><span class="line"><span class="built_in">Type</span>: alloc_space</span><br><span class="line">Time: Jan <span class="number">25</span>, <span class="number">2019</span> at <span class="number">3</span>:<span class="number">02</span>pm (CST)</span><br><span class="line">Entering interactive mode (<span class="built_in">type</span> <span class="string">&quot;help&quot;</span> <span class="keyword">for</span> commands, <span class="string">&quot;o&quot;</span> <span class="keyword">for</span> options)</span><br><span class="line">(pprof) top</span><br><span class="line">Showing nodes accounting <span class="keyword">for</span> <span class="number">604.62</span>GB, <span class="number">44.50</span>% of <span class="number">1358.61</span>GB total</span><br><span class="line">Dropped <span class="number">776</span> nodes (cum &lt;= <span class="number">6.79</span>GB)</span><br><span class="line">Showing top <span class="number">10</span> nodes out of <span class="number">155</span></span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">  <span class="number">111.40</span>GB  <span class="number">8.20</span>%  <span class="number">8.20</span>%   <span class="number">111.40</span>GB  <span class="number">8.20</span>%  bytes.makeSlice</span><br><span class="line">  <span class="number">107.72</span>GB  <span class="number">7.93</span>% <span class="number">16.13</span>%   <span class="number">107.72</span>GB  <span class="number">7.93</span>%  github.com/sirupsen/logrus.(*Entry).WithFields</span><br><span class="line">   <span class="number">65.94</span>GB  <span class="number">4.85</span>% <span class="number">20.98</span>%    <span class="number">65.94</span>GB  <span class="number">4.85</span>%  strings.Replace</span><br><span class="line">   <span class="number">54.10</span>GB  <span class="number">3.98</span>% <span class="number">24.96</span>%    <span class="number">56.03</span>GB  <span class="number">4.12</span>%  github.com/json<span class="literal">-iterator</span>/go.(*frozenConfig).Marshal</span><br><span class="line">   <span class="number">47.54</span>GB  <span class="number">3.50</span>% <span class="number">28.46</span>%    <span class="number">47.54</span>GB  <span class="number">3.50</span>%  net/url.unescape</span><br><span class="line">   <span class="number">47.11</span>GB  <span class="number">3.47</span>% <span class="number">31.93</span>%    <span class="number">48.16</span>GB  <span class="number">3.55</span>%  github.com/json<span class="literal">-iterator</span>/go.(*Iterator).readStringSlowPath</span><br><span class="line">   <span class="number">46.63</span>GB  <span class="number">3.43</span>% <span class="number">35.36</span>%   <span class="number">103.04</span>GB  <span class="number">7.58</span>%  handlers.(*AdserviceHandler).returnAd</span><br><span class="line">   <span class="number">42.43</span>GB  <span class="number">3.12</span>% <span class="number">38.49</span>%    <span class="number">84.62</span>GB  <span class="number">6.23</span>%  models.LogItemsToBytes</span><br><span class="line">   <span class="number">42.22</span>GB  <span class="number">3.11</span>% <span class="number">41.59</span>%    <span class="number">42.22</span>GB  <span class="number">3.11</span>%  strings.Join</span><br><span class="line">   <span class="number">39.52</span>GB  <span class="number">2.91</span>% <span class="number">44.50</span>%    <span class="number">87.06</span>GB  <span class="number">6.41</span>%  net/url.parseQuery</span><br></pre></td></tr></table></figure><p>从结果中可以看出采集期间一共分配了 <code>1358.61GB</code> <code>top 10</code> 占用了 <code>44.50%</code> 其中 <code>bytes.makeSlice</code> 占了接近 <code>1/10</code>，那么看看都是谁在调用 <code>bytes.makeSlice</code> 吧。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(pprof) web bytes.makeSlice</span><br></pre></td></tr></table></figure><p><img src="/assets/image/20190126/1.jpg" alt="image-1"></p><p>从上图可以看出调用 <code>bytes.makeSlice</code> 的最终方法是 <code>ioutil.ReadAll</code>, (受篇幅影响就没有截取 <code>ioutil.ReadAll</code> 上面的方法了)，而 90% 都是 <code>ioutil.ReadAll</code> 读取 <code>http</code> 数据调用，找到地方先别急想优化方案，先看看为啥 <code>ioutil.ReadAll</code> 会导致这么多内存分配。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">readAll</span><span class="params">(r io.Reader, capacity <span class="keyword">int64</span>)</span> <span class="params">(b []<span class="keyword">byte</span>, err error)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> buf bytes.Buffer</span><br><span class="line"><span class="comment">// If the buffer overflows, we will get bytes.ErrTooLarge.</span></span><br><span class="line"><span class="comment">// Return that as an error. Any other panic remains.</span></span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">e := <span class="built_in">recover</span>()</span><br><span class="line"><span class="keyword">if</span> e == <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> panicErr, ok := e.(error); ok &amp;&amp; panicErr == bytes.ErrTooLarge &#123;</span><br><span class="line">err = panicErr</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="built_in">panic</span>(e)</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">int64</span>(<span class="keyword">int</span>(capacity)) == capacity &#123;</span><br><span class="line">buf.Grow(<span class="keyword">int</span>(capacity))</span><br><span class="line">&#125;</span><br><span class="line">_, err = buf.ReadFrom(r)</span><br><span class="line"><span class="keyword">return</span> buf.Bytes(), err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReadAll</span><span class="params">(r io.Reader)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> readAll(r, bytes.MinRead)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上是标准库 <code>ioutil.ReadAll</code> 的代码，每次会创建一个 <code>var buf bytes.Buffer</code> 并且初始化 <code>buf.Grow(int(capacity))</code> 的大小为 <code>bytes.MinRead</code>, 这个值呢就是 <code>512</code>，按这个 <code>buffer</code> 的大小读取一次数据需要分配 2~16 次内存，天啊简直不能忍，我自己创建一个 <code>buffer</code> 好不好。</p><p>看一下火焰图🔥吧，其中红框标记的就是 <code>ioutil.ReadAll</code> 的部分，颜色比较鲜艳。</p><p><img src="/assets/image/20190126/2.jpg" alt="image-2"></p><h2 id="优化读取方法"><a href="#优化读取方法" class="headerlink" title="优化读取方法"></a>优化读取方法</h2><p>自己创建足够大的 <code>buffer</code> 减少因为容量不够导致的多次扩容问题。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">buffer := bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">4096</span>))</span><br><span class="line">_, err := io.Copy(buffer, request.Body)</span><br><span class="line"><span class="keyword">if</span> err !=<span class="literal">nil</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>恩恩这样应该差不多了，为啥是初始化 <code>4096</code> 的大小，这是个均值，即使比 <code>4096</code> 大基本也就多分配一次内存即可，而且大多数数据都是比 <code>4096</code> 小的。</p><p>但是这样真的就算好了吗，当然不能这样，这个 <code>buffer</code> 个每请求都要创建一次，是不是应该考虑一下复用呢，使用 <code>sync.Pool</code> 建立一个缓冲池效果就更好了。</p><p>以下是优化读取请求的简化代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> adapter</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/json-iterator/go&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/sirupsen/logrus&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exbytes&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Adapter <span class="keyword">struct</span> &#123;</span><br><span class="line">pool sync.Pool</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">()</span> *<span class="title">Adapter</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;Adapter&#123;</span><br><span class="line">pool: sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">4096</span>))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(api *Adapter)</span> <span class="title">GetRequest</span><span class="params">(r *http.Request)</span> <span class="params">(*Request, error)</span></span> &#123;</span><br><span class="line">buffer := api.pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> buffer != <span class="literal">nil</span> &#123;</span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line">_, err := io.Copy(buffer, r.Body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">request := &amp;Request&#123;&#125;</span><br><span class="line"><span class="keyword">if</span> err = jsoniter.Unmarshal(buffer.Bytes(), request); err != <span class="literal">nil</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;json&quot;</span>: exbytes.ToString(buffer.Bytes()),</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;jsoniter.UnmarshalJSON fail. error:%v&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ....</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> request, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用 <code>sync.Pool</code> 的方式是不是有点怪，主要是 <code>defer</code> 和 <code>api.pool.Put(buffer)；buffer = nil</code> 这里解释一下，为了提高 <code>buufer</code> 的复用率会在不使用时尽快把 <code>buffer</code> 放回到缓冲池中，<code>defer</code> 之所以会判断 <code>buffer != nil</code> 主要是在业务逻辑出现错误时，但是 <code>buffer</code> 还没有放回缓冲池时把 <code>buffer</code> 放回到缓冲池，因为在每个错误处理之后都写 <code>api.pool.Put(buffer)</code> 不是一个好的方法，而且容易忘记，但是如果在确定不再使用时 <code>api.pool.Put(buffer)；buffer = nil</code> 就可以尽早把 <code>buffer</code> 放回到缓冲池中，提高复用率，减少新建 <code>buffer</code>。</p><p>这样就好了吗，别急，之前说服务里面还会构建请求，看看构建请求如何优化吧。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> adapter</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"><span class="string">&quot;io/ioutil&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/json-iterator/go&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/sirupsen/logrus&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exbytes&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Adapter <span class="keyword">struct</span> &#123;</span><br><span class="line">pool sync.Pool</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">()</span> *<span class="title">Adapter</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;Adapter&#123;</span><br><span class="line">pool: sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">4096</span>))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(api *Adapter)</span> <span class="title">Request</span><span class="params">(r *Request)</span> <span class="params">(*Response, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> err error</span><br><span class="line">buffer := api.pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> buffer != <span class="literal">nil</span> &#123;</span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line">e := jsoniter.NewEncoder(buffer)</span><br><span class="line">err = e.Encode(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;request&quot;</span>: r,</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;jsoniter.Marshal failure: %v&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;jsoniter.Marshal failure: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">data := buffer.Bytes()</span><br><span class="line">req, err := http.NewRequest(<span class="string">&quot;POST&quot;</span>, <span class="string">&quot;http://xxx.com&quot;</span>, buffer)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;data&quot;</span>: exbytes.ToString(data),</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;http.NewRequest failed: %v&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;http.NewRequest failed: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">req.Header.Set(<span class="string">&quot;User-Agent&quot;</span>, <span class="string">&quot;xxx&quot;</span>)</span><br><span class="line"></span><br><span class="line">httpResponse, err := http.DefaultClient.Do(req)</span><br><span class="line"><span class="keyword">if</span> httpResponse != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">io.Copy(ioutil.Discard, httpResponse.Body)</span><br><span class="line">httpResponse.Body.Close()</span><br><span class="line">&#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;url&quot;</span>: <span class="string">&quot;http://xxx.com&quot;</span>,</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;query service failed %v&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;query service failed %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> httpResponse.StatusCode != <span class="number">200</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;url&quot;</span>:         <span class="string">&quot;http://xxx.com&quot;</span>,</span><br><span class="line"><span class="string">&quot;status&quot;</span>:      httpResponse.Status,</span><br><span class="line"><span class="string">&quot;status_code&quot;</span>: httpResponse.StatusCode,</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;invalid http status code&quot;</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;invalid http status code&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">buffer.Reset()</span><br><span class="line">_, err = io.Copy(buffer, httpResponse.Body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;adapter io.copy failure error:%v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">respData := buffer.Bytes()</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;response_json&quot;</span>: exbytes.ToString(respData),</span><br><span class="line">&#125;).Debug(<span class="string">&quot;response json&quot;</span>)</span><br><span class="line"></span><br><span class="line">res := &amp;Response&#123;&#125;</span><br><span class="line">err = jsoniter.Unmarshal(respData, res)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">logrus.WithFields(logrus.Fields&#123;</span><br><span class="line"><span class="string">&quot;data&quot;</span>: exbytes.ToString(respData),</span><br><span class="line"><span class="string">&quot;url&quot;</span>:  <span class="string">&quot;http://xxx.com&quot;</span>,</span><br><span class="line">&#125;).Errorf(<span class="string">&quot;adapter jsoniter.Unmarshal failed, error:%v&quot;</span>, err)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;adapter jsoniter.Unmarshal failed, error:%v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">return</span> res, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个示例和之前差不多，只是不仅用来读取 <code>http.Response.Body</code> 还用来创建一个 <code>jsoniter.NewEncoder</code> 用来把请求压缩成 <code>json</code> 字符串，并且作为 <code>http.NewRequest</code> 的 <code>body</code> 参数， 如果直接用 <code>jsoniter.Marshal</code> 同样会创建很多次内存，<code>jsoniter</code> 也使用 <code>buffer</code> 做为缓冲区，并且默认大小为 <code>512</code>, 代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cfg Config)</span> <span class="title">Froze</span><span class="params">()</span> <span class="title">API</span></span> &#123;</span><br><span class="line">api := &amp;frozenConfig&#123;</span><br><span class="line">sortMapKeys:                   cfg.SortMapKeys,</span><br><span class="line">indentionStep:                 cfg.IndentionStep,</span><br><span class="line">objectFieldMustBeSimpleString: cfg.ObjectFieldMustBeSimpleString,</span><br><span class="line">onlyTaggedField:               cfg.OnlyTaggedField,</span><br><span class="line">disallowUnknownFields:         cfg.DisallowUnknownFields,</span><br><span class="line">&#125;</span><br><span class="line">api.streamPool = &amp;sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> NewStream(api, <span class="literal">nil</span>, <span class="number">512</span>)</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// .....</span></span><br><span class="line"><span class="keyword">return</span> api</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而且序列化之后会进行一次数据拷贝：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cfg *frozenConfig)</span> <span class="title">Marshal</span><span class="params">(v <span class="keyword">interface</span>&#123;&#125;)</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">stream := cfg.BorrowStream(<span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">defer</span> cfg.ReturnStream(stream)</span><br><span class="line">stream.WriteVal(v)</span><br><span class="line"><span class="keyword">if</span> stream.Error != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, stream.Error</span><br><span class="line">&#125;</span><br><span class="line">result := stream.Buffer()</span><br><span class="line">copied := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="built_in">len</span>(result))</span><br><span class="line"><span class="built_in">copy</span>(copied, result)</span><br><span class="line"><span class="keyword">return</span> copied, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>既然要用 <code>buffer</code> 那就一起吧^_^，这样可以减少多次内存分配，下读取 <code>http.Response.Body</code> 之前一定要记得 <code>buffer.Reset()</code>， 这样基本就已经完成了 <code>http.Request.Body</code> 和 <code>http.Response.Body</code> 的数据读取优化了，具体效果等上线跑一段时间稳定之后来查看吧。</p><h2 id="效果分析"><a href="#效果分析" class="headerlink" title="效果分析"></a>效果分析</h2><p>上线跑了一天，来看看效果吧</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$</span> go tool pprof allocs2</span><br><span class="line">File: connect_server</span><br><span class="line"><span class="built_in">Type</span>: alloc_space</span><br><span class="line">Time: Jan <span class="number">26</span>, <span class="number">2019</span> at <span class="number">10</span>:<span class="number">27</span>am (CST)</span><br><span class="line">Entering interactive mode (<span class="built_in">type</span> <span class="string">&quot;help&quot;</span> <span class="keyword">for</span> commands, <span class="string">&quot;o&quot;</span> <span class="keyword">for</span> options)</span><br><span class="line">(pprof) top</span><br><span class="line">Showing nodes accounting <span class="keyword">for</span> <span class="number">295.40</span>GB, <span class="number">40.62</span>% of <span class="number">727.32</span>GB total</span><br><span class="line">Dropped <span class="number">738</span> nodes (cum &lt;= <span class="number">3.64</span>GB)</span><br><span class="line">Showing top <span class="number">10</span> nodes out of <span class="number">174</span></span><br><span class="line">      flat  flat%   sum%        cum   cum%</span><br><span class="line">   <span class="number">73.52</span>GB <span class="number">10.11</span>% <span class="number">10.11</span>%    <span class="number">73.52</span>GB <span class="number">10.11</span>%  git.tvblack.com/tvblack/connect_server/vendor/github.com/sirupsen/logrus.(*Entry).WithFields</span><br><span class="line">   <span class="number">31.70</span>GB  <span class="number">4.36</span>% <span class="number">14.47</span>%    <span class="number">31.70</span>GB  <span class="number">4.36</span>%  net/url.unescape</span><br><span class="line">   <span class="number">27.49</span>GB  <span class="number">3.78</span>% <span class="number">18.25</span>%    <span class="number">54.87</span>GB  <span class="number">7.54</span>%  git.tvblack.com/tvblack/connect_server/models.LogItemsToBytes</span><br><span class="line">   <span class="number">27.41</span>GB  <span class="number">3.77</span>% <span class="number">22.01</span>%    <span class="number">27.41</span>GB  <span class="number">3.77</span>%  strings.Join</span><br><span class="line">   <span class="number">25.04</span>GB  <span class="number">3.44</span>% <span class="number">25.46</span>%    <span class="number">25.04</span>GB  <span class="number">3.44</span>%  bufio.NewWriterSize</span><br><span class="line">   <span class="number">24.81</span>GB  <span class="number">3.41</span>% <span class="number">28.87</span>%    <span class="number">24.81</span>GB  <span class="number">3.41</span>%  bufio.NewReaderSize</span><br><span class="line">   <span class="number">23.91</span>GB  <span class="number">3.29</span>% <span class="number">32.15</span>%    <span class="number">23.91</span>GB  <span class="number">3.29</span>%  regexp.(*bitState).reset</span><br><span class="line">   <span class="number">23.06</span>GB  <span class="number">3.17</span>% <span class="number">35.32</span>%    <span class="number">23.06</span>GB  <span class="number">3.17</span>%  math/big.nat.make</span><br><span class="line">   <span class="number">19.90</span>GB  <span class="number">2.74</span>% <span class="number">38.06</span>%    <span class="number">20.35</span>GB  <span class="number">2.80</span>%  git.tvblack.com/tvblack/connect_server/vendor/github.com/json<span class="literal">-iterator</span>/go.(*Iterator).readStringSlowPath</span><br><span class="line">   <span class="number">18.58</span>GB  <span class="number">2.56</span>% <span class="number">40.62</span>%    <span class="number">19.12</span>GB  <span class="number">2.63</span>%  net/textproto.(*Reader).ReadMIMEHeader</span><br></pre></td></tr></table></figure><p>哇塞 <code>bytes.makeSlice</code> 终于从前十中消失了，真的太棒了，还是看看 <code>bytes.makeSlice</code> 的其它调用情况吧。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(pprof) web bytes.makeSlice</span><br></pre></td></tr></table></figure><p><img src="/assets/image/20190126/3.jpg" alt="image-3"></p><p>从图中可以发现 <code>bytes.makeSlice</code> 的分配已经很小了， 且大多数是 <code>http.Request.ParseForm</code> 读取 <code>http.Request.Body</code> 使用 <code>ioutil.ReadAll</code> 原因，这次优化的效果非常的好。</p><p>看一下更直观的火焰图🔥吧，和优化前对比一下很明显 <code>ioutil.ReadAll</code> 看不到了</p><p><img src="/assets/image/20190126/4.jpg" alt="image-4"></p><h2 id="优化期间遇到的问题"><a href="#优化期间遇到的问题" class="headerlink" title="优化期间遇到的问题"></a>优化期间遇到的问题</h2><p>比较惭愧在优化的过程出现了一个过失，导致生产环境2分钟故障，通过自动部署立即回滚才得以快速恢复，之后分析代码解决之后上线才完美优化，下面总结一下出现的问题吧。</p><p>在构建 <code>http</code> 请求时我分了两个部分优化，序列化 <code>json</code> 和读取 <code>http.Response.Body</code> 数据，保持一个观点就是尽早把 <code>buffer</code> 放回到缓冲池，因为 <code>http.DefaultClient.Do(req)</code> 是网络请求会相对耗时，在这个之前我把 <code>buffer</code> 放回到缓冲池中，之后读取 <code>http.Response.Body</code> 时在重新获取一个 <code>buffer</code>，大概代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> adapter</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;bytes&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;io&quot;</span></span><br><span class="line"><span class="string">&quot;io/ioutil&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/json-iterator/go&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/sirupsen/logrus&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/thinkeridea/go-extend/exbytes&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Adapter <span class="keyword">struct</span> &#123;</span><br><span class="line">pool sync.Pool</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">()</span> *<span class="title">Adapter</span></span> &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;Adapter&#123;</span><br><span class="line">pool: sync.Pool&#123;</span><br><span class="line">New: <span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="keyword">return</span> bytes.NewBuffer(<span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">4096</span>))</span><br><span class="line">&#125;,</span><br><span class="line">&#125;,</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(api *Adapter)</span> <span class="title">Request</span><span class="params">(r *Request)</span> <span class="params">(*Response, error)</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> err error</span><br><span class="line">buffer := api.pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> buffer != <span class="literal">nil</span> &#123;</span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line">e := jsoniter.NewEncoder(buffer)</span><br><span class="line">err = e.Encode(r)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;jsoniter.Marshal failure: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">data := buffer.Bytes()</span><br><span class="line">req, err := http.NewRequest(<span class="string">&quot;POST&quot;</span>, <span class="string">&quot;http://xxx.com&quot;</span>, buffer)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;http.NewRequest failed: %v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">req.Header.Set(<span class="string">&quot;User-Agent&quot;</span>, <span class="string">&quot;xxx&quot;</span>)</span><br><span class="line"></span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">httpResponse, err := http.DefaultClient.Do(req)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// ....</span></span><br><span class="line"></span><br><span class="line">buffer = api.pool.Get().(*bytes.Buffer)</span><br><span class="line">buffer.Reset()</span><br><span class="line"><span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> buffer != <span class="literal">nil</span> &#123;</span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">_, err = io.Copy(buffer, httpResponse.Body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">&quot;adapter io.copy failure error:%v&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ....</span></span><br><span class="line"></span><br><span class="line">api.pool.Put(buffer)</span><br><span class="line">buffer = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">return</span> res, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上线之后马上发生了错误 <code>http: ContentLength=2090 with Body length 0</code> 发送请求的时候从 <code>buffer</code> 读取数据发现数据不见了或者数据不够了，我去这是什么鬼，马上回滚恢复业务，然后分析 <code>http.DefaultClient.Do(req)</code> 和 <code>http.NewRequest</code>，在调用 <code>http.NewRequest</code> 是并没有从 <code>buffer</code> 读取数据，而只是创建了一个 <code>req.GetBody</code> 之后在 <code>http.DefaultClient.Do</code> 是才读取数据，因为在 <code>http.DefaultClient.Do</code> 之前把 <code>buffer</code> 放回到缓冲池中，其它 <code>goroutine</code> 获取到 <code>buffer</code> 并进行 <code>Reset</code> 就发生了数据争用，当然会导致数据读取不完整了，真实汗颜，对 <code>http.Client</code> 了解太少，争取有空撸一遍源码。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>使用合适大小的 <code>buffer</code> 来减少内存分配，<code>sync.Pool</code> 可以帮助复用 <code>buffer</code>， 一定要自己写这些逻辑，避免使用三方包，三方包即使使用同样的技巧为了避免数据争用，在返回数据时候必然会拷贝一个新的数据返回，就像 <code>jsoniter</code> 虽然使用了 <code>sync.Pool</code> 和 <code>buffer</code> 但是返回数据时还需要拷贝，另外这种通用包并不能给一个非常贴合业务的初始 <code>buffer</code> 大小，过小会导致数据发生拷贝，过大会太过浪费内存。</p><p>程序中善用 <code>buffer</code> 和 <code>sync.Pool</code> 可以大大的改善程序的性能，并且这两个组合在一起使用非常的简单，并不会使代码变的复杂。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;从 &lt;code&gt;http.Request.Body&lt;/code&gt; 或 &lt;code&gt;http.Response.Body&lt;/code&gt; 中读取数据方法或许很多，标准库中大多数使用 &lt;code&gt;ioutil.ReadAll&lt;/code&gt; 方法一次读取所有数据，如果是 &lt;code&gt;json&lt;/code&gt; 格式的数据还可以使用 &lt;code&gt;json.NewDecoder&lt;/code&gt; 从 &lt;code&gt;io.Reader&lt;/code&gt; 创建一个解析器，假使使用 &lt;code&gt;pprof&lt;/code&gt; 来分析程序总是会发现 &lt;code&gt;bytes.makeSlice&lt;/code&gt; 分配了大量内存，且总是排行第一，今天就这个问题来说一下如何高效优雅的读取 &lt;code&gt;http&lt;/code&gt; 中的数据。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="net.http" scheme="https://blog.thinkeridea.com/tags/net-http/"/>
    
    <category term="http" scheme="https://blog.thinkeridea.com/tags/http/"/>
    
    <category term="pool" scheme="https://blog.thinkeridea.com/tags/pool/"/>
    
    <category term="bytes" scheme="https://blog.thinkeridea.com/tags/bytes/"/>
    
    <category term="buffer" scheme="https://blog.thinkeridea.com/tags/buffer/"/>
    
    <category term="makeSlice" scheme="https://blog.thinkeridea.com/tags/makeslice/"/>
    
    <category term="ioutil" scheme="https://blog.thinkeridea.com/tags/ioutil/"/>
    
  </entry>
  
  <entry>
    <title>【Go】slice的一些使用技巧</title>
    <link href="https://blog.thinkeridea.com/201901/go/slice_de_yi_xie_shi_yong_ji_qiao.html"/>
    <id>https://blog.thinkeridea.com/201901/go/slice_de_yi_xie_shi_yong_ji_qiao.html</id>
    <published>2019-01-22T22:34:57.000Z</published>
    <updated>2019-02-24T12:53:49.161Z</updated>
    
    <content type="html"><![CDATA[<p><code>slice</code> 是 <code>Go</code> 语言十分重要的数据类型，它承载着很多使命，从语言层面来看是 <code>Go</code> 语言的内置数据类型，从数据结构来看是动态长度的顺序链表，由于 <code>Go</code> 不能直接操作内存（通过系统调用可以实现，但是语言本身并不支持），往往 <code>slice</code> 也可以用来帮助开发者申请大块内存实现缓冲、缓存等功能。</p><p>在 <code>Go</code> 语言项目中大量的使用 <code>slice</code>, 我总结三年来对 <code>slice</code> 的一些操作技巧，以方便可以高效的使用 <code>slice</code>， 并使用 <code>slice</code> 解决一些棘手的问题。</p><h2 id="slice-的基本操作"><a href="#slice-的基本操作" class="headerlink" title="slice 的基本操作"></a>slice 的基本操作</h2><p>先熟悉一些 <code>slice</code> 的基本的操作, 对最常规的 <code>:</code> 操作就可玩出很多花样。</p><ul><li><code>s=ss[:]</code> 引用一个切片或数组</li><li><code>s=s[:0]</code> 清空切片</li><li><code>s=s[:10]</code> <code>s=s[10:]</code> <code>s=s[10:20]</code> 截取接片</li><li><code>s=ss[0:10:20]</code> 从切片或数组引用指定长度和容量的切片</li></ul><p>下标索引操作的一些误区 <code>s[i:l:c]</code> <code>i</code> 是起始偏移的起始位置，<code>l</code> 是起始偏移的长度结束位置， <code>l-i</code> 就是新 <code>slice</code> 的长度， <code>c</code> 是起始偏移的容量结束位置，<code>c-i</code> 就是新 <code>slice</code> 的容量。其中 <code>i</code> 、<code>l</code> 、<code>c</code> 并不是当前 <code>slice</code> 的索引，而是引用底层数组相对当前 <code>slice</code> 起始位置的偏移量，所以是可超出当前 <code>slice</code> 的长度的， 但不能超出当前 <code>slice</code> 的容量，如下操作是合法的：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">100</span>)</span><br><span class="line">s[<span class="number">20</span>] = <span class="number">100</span></span><br><span class="line">s1 := s[<span class="number">10</span>:<span class="number">10</span>]</span><br><span class="line">s2 := s1[<span class="number">10</span>:<span class="number">20</span>]</span><br><span class="line">fmt.Println(s1)</span><br><span class="line">fmt.Println(s2)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>s1</code> 是 <code>[]</code>；<code>s2</code> 是 <code>[100 0 0 0 0 0 0 0 0 0]</code>, 这里并不会发生下标越界的情况，一个更好的例子在 <a href="#csv-reader-%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E4%BE%8B%E5%AD%90">csv reader 中的一个例子</a></p><a id="more"></a><p><strong>创建 slice</strong></p><p>创建切片的方法有很多，下面罗列一些常规的：</p><ul><li><code>var s []int</code> 创建 nil切片</li><li><code>s := make([]int, 0, 0)</code> 、 <code>s=[]int&#123;&#125;</code> 创建无容量空切片</li><li><code>s:= make([]int, 0, 100)</code> 创建有容量空切片</li><li><code>s:=make([]int, 100)</code> 创建零值切片</li><li><code>s:=array[:]</code> 引用数组创建切片</li></ul><p><strong>内置函数</strong></p><ul><li><code>len(s)</code> 获取切片的长度</li><li><code>cap(s)</code> 获取切片的容量</li><li><code>append(s, ...)</code> 向切片追加内容</li><li><code>copy(s, s1)</code> 向切片拷贝内容</li></ul><h2 id="一个缓冲的简单示例"><a href="#一个缓冲的简单示例" class="headerlink" title="一个缓冲的简单示例"></a>一个缓冲的简单示例</h2><p>遇到过很多拼接字符串的方法，各种各样的都有 <code>fmt</code> <code>builder</code> <code>buffer</code> <code>+</code> 等等，实际上 <code>builder</code> 和 <code>buffer</code> 都是使用 <code>[]byte</code> 的切片作为缓冲来实现的，<code>fmt</code> 往往性能最差，原因是它主要功能不是连接字符串而是格式化数据会用到反射等等操作。<code>+</code> 操作在大量拼接时性能也是很差， 不过小字符串少量拼接效果很理想，<code>builder</code> 往往性能不如 <code>buffer</code> 特别是在较短字符串拼接上，实际 <code>builder</code> 和 <code>buffer</code> 实现原理非常类似，<code>builder</code> 在转成字符串时使用了 <code>unsafe</code> 减少了一次内存分配，因为小字符串因为扩容机制不如 <code>buffer</code> 灵活，所以性能有所不如，大字符串降低一次大的内存分配就显得很明显了。</p><p>经常遇到一个需求就是拼接 <code>[]int</code> 中个各个元素，很多种实现都有人用，都是需要遍历转换 <code>int</code> 到 <code>string</code>，但是拼接方法千奇百怪，以下提供两种方法对比（<a href="https://github.com/thinkeridea/example/blob/master/slice/slice_int_to_string.go">源码在GitHub</a>）。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> slice</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;strconv&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceInt2String1</span><span class="params">(s []<span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(s) &lt; <span class="number">1</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">ss := strconv.Itoa(s[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">ss += <span class="string">&quot;,&quot;</span> + strconv.Itoa(s[i])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ss</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceInt2String2</span><span class="params">(s []<span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(s) &lt; <span class="number">1</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">0</span>, <span class="number">256</span>)</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(s[<span class="number">0</span>])...)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">b = <span class="built_in">append</span>(b, <span class="string">&#x27;,&#x27;</span>)</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(s[i])...)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="keyword">string</span>(b)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceInt2String3</span><span class="params">(s []<span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(s) &lt; <span class="number">1</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">0</span>, <span class="number">256</span>)</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(s[<span class="number">0</span>])...)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="built_in">len</span>(s); i++ &#123;</span><br><span class="line">b = <span class="built_in">append</span>(b, <span class="string">&#x27;,&#x27;</span>)</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(s[i])...)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> *(*<span class="keyword">string</span>)(unsafe.Pointer(&amp;b))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>SliceInt2String1</code> 使用原始的 <code>+</code> 操作，因为是较小的字符串拼接，使用 <code>+</code> 主要是因为在小字符串拼接性能优于其它几种方法，<code>SliceInt2String2</code> 与 <code>SliceInt2String3</code> 都使用了一个 <code>256</code> 容量的 <code>[]byte</code> 作为缓冲， 唯一的区别是在返回时一个使用 <code>string</code> 转换类型，一个使用 <code>unsafe</code> 转换类型。</p><p>写了一个性能测试（<a href="https://github.com/thinkeridea/example/blob/master/slice/slice_int_to_string_test.go">源码在GitHub</a>），看一下效果吧:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice</span><br><span class="line">BenchmarkSliceInt2String1-8    3000000       461 ns&#x2F;op     144 B&#x2F;op       9 allocs&#x2F;op</span><br><span class="line">BenchmarkSliceInt2String2-8   20000000       117 ns&#x2F;op      32 B&#x2F;op       1 allocs&#x2F;op</span><br><span class="line">BenchmarkSliceInt2String3-8   10000000       144 ns&#x2F;op     256 B&#x2F;op       1 allocs&#x2F;op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice5.928s</span><br></pre></td></tr></table></figure><p>明显可以看得出 <code>SliceInt2String2</code> 的性能是 <code>SliceInt2String1</code> 7倍左右，提升很明显，<code>SliceInt2String2</code> 与 <code>SliceInt2String3</code> 差异很小，主要是因为使用 <code>unsafe</code>  转换类型导致大内存无法释放，实际这个测试中连接字符串只需要 <code>32</code> 个字节，使用 <code>unsafe</code> 却导致 <code>256</code> 个字节无法被释放，这也正是 <code>builder</code> 和 <code>buffer</code> 的差别，所以小字符串拼接 <code>buffer</code> 性能往往更好。在这里简单的通过 <code>[]byte</code> 减少内存分配次数来实现缓冲。</p><p>如果连续拼接一组这样的操作，比如输入 <code>[][]int</code>,  输出 <code>[]string</code> （<a href="https://github.com/thinkeridea/example/blob/master/slice/slice_int_to_string.go#L52">源码在GitHub</a>）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> slice</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;strconv&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceInt2String4</span><span class="params">(s [][]<span class="keyword">int</span>)</span> []<span class="title">string</span></span> &#123;</span><br><span class="line">res := <span class="built_in">make</span>([]<span class="keyword">string</span>, <span class="built_in">len</span>(s))</span><br><span class="line"><span class="keyword">for</span> i, v := <span class="keyword">range</span> s &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(v) &lt; <span class="number">1</span> &#123;</span><br><span class="line">res[i] = <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">res[i] += strconv.Itoa(v[<span class="number">0</span>])</span><br><span class="line"><span class="keyword">for</span> j := <span class="number">1</span>; j &lt; <span class="built_in">len</span>(v); j++ &#123;</span><br><span class="line">res[i] += <span class="string">&quot;,&quot;</span> + strconv.Itoa(v[j])</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> res</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SliceInt2String5</span><span class="params">(s [][]<span class="keyword">int</span>)</span> []<span class="title">string</span></span> &#123;</span><br><span class="line">res := <span class="built_in">make</span>([]<span class="keyword">string</span>, <span class="built_in">len</span>(s))</span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">0</span>, <span class="number">256</span>)</span><br><span class="line"><span class="keyword">for</span> i, v := <span class="keyword">range</span> s &#123;</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(v) &lt; <span class="number">1</span> &#123;</span><br><span class="line">res[i] = <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="keyword">continue</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">b = b[:<span class="number">0</span>]</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(v[<span class="number">0</span>])...)</span><br><span class="line"><span class="keyword">for</span> j := <span class="number">1</span>; j &lt; <span class="built_in">len</span>(v); j++ &#123;</span><br><span class="line">b = <span class="built_in">append</span>(b, <span class="string">&#x27;,&#x27;</span>)</span><br><span class="line">b = <span class="built_in">append</span>(b, strconv.Itoa(v[j])...)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">res[i] = <span class="keyword">string</span>(b)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> res</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>SliceInt2String5</code> 中使用 <code>b = b[:0]</code>  来促使达到反复使用一块缓冲区，写了一个性能测试（<a href="https://github.com/thinkeridea/example/blob/master/slice/slice_int_to_string_test.go#L85">源码在GitHub</a>），看一下效果吧:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice</span><br><span class="line">BenchmarkSliceInt2String4-8     300000      4420 ns&#x2F;op    1440 B&#x2F;op      82 allocs&#x2F;op</span><br><span class="line">BenchmarkSliceInt2String5-8    1000000      1102 ns&#x2F;op     432 B&#x2F;op      10 allocs&#x2F;op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice8.364s</span><br></pre></td></tr></table></figure><p>较 <code>+</code> 版本提升接近4倍的性能，这是使用 <code>slice</code> 作为缓冲区极好的技巧，使用非常方便，并不用使用 <code>builder</code> 和 <code>buffer</code>， <code>slice</code> 操作非常的简单实用。</p><h2 id="append-与-copy"><a href="#append-与-copy" class="headerlink" title="append 与 copy"></a>append 与 copy</h2><p>如果合并多个 <code>slice</code> 为一个，有三种方式来合并，主要合并差异来源于创建新 <code>slice</code> 的方法，使用 <code>var news []int</code> 或者 <code>news:=make([]int, 0, len(s1)+len(s2)....)</code> 的方式创建的新变量就需要使用 <code>append</code> 来合并，如果使用 <code>news:=make([]int, len(s1)+len(s2)....)</code> 就需要使用 <code>copy</code> 来合并。不同的方法也有差异，<code>append</code> 和 <code>copy</code> 在这个例子中主要差异在于 <code>append</code> 适用于零长度的初始化 <code>slice</code>， <code>copy</code> 适用于确定长度的 <code>slice</code>。</p><p>写了一个测试来看看两者的差异吧（<a href="https://github.com/thinkeridea/example/blob/master/slice/append_and_copy_test.go">源码在GitHub</a>）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkExperiment3Append1</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line"><span class="keyword">var</span> s []<span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> j := <span class="number">0</span>; j &lt; <span class="number">20</span>; j++ &#123;</span><br><span class="line">s = <span class="built_in">append</span>(s, []<span class="keyword">int</span>&#123;j, j + <span class="number">1</span>, j + <span class="number">2</span>, j + <span class="number">3</span>, j + <span class="number">4</span>&#125;...)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkExperiment3Append2</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">100</span>)</span><br><span class="line"><span class="keyword">for</span> j := <span class="number">0</span>; j &lt; <span class="number">20</span>; j++ &#123;</span><br><span class="line">s = <span class="built_in">append</span>(s, []<span class="keyword">int</span>&#123;j, j + <span class="number">1</span>, j + <span class="number">2</span>, j + <span class="number">3</span>, j + <span class="number">4</span>&#125;...)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkExperiment3Copy</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">100</span>)</span><br><span class="line">n := <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> j := <span class="number">0</span>; j &lt; <span class="number">20</span>; j++ &#123;</span><br><span class="line">n += <span class="built_in">copy</span>(s[n:], []<span class="keyword">int</span>&#123;j, j + <span class="number">1</span>, j + <span class="number">2</span>, j + <span class="number">3</span>, j + <span class="number">4</span>&#125;)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">goos: darwin</span><br><span class="line">goarch: amd64</span><br><span class="line">pkg: github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice</span><br><span class="line">BenchmarkExperiment3Append1-8    2000000       782 ns&#x2F;op    3024 B&#x2F;op       6 allocs&#x2F;op</span><br><span class="line">BenchmarkExperiment3Append2-8   10000000       192 ns&#x2F;op       0 B&#x2F;op       0 allocs&#x2F;op</span><br><span class="line">BenchmarkExperiment3Copy-8      10000000       217 ns&#x2F;op       0 B&#x2F;op       0 allocs&#x2F;op</span><br><span class="line">PASS</span><br><span class="line">ok  github.com&#x2F;thinkeridea&#x2F;example&#x2F;slice6.926s</span><br></pre></td></tr></table></figure><p>从结果上来看使用没有容量的 <code>append</code> 性能真的很糟糕，实际上不要对没有任何容量的 <code>slice</code> 进行 <code>append</code> 操作是最好的实践，在准备用 <code>append</code> 的时候应该预先给定一个容量，哪怕这个容量并不是确定的，像前面缓存连接字符串时一样，并不能明确使用的空间，先分配256个字节，这样的好处是可以减少系统调用分配内存的次数，即使空间不能用完，也不用太过担心浪费，<code>append</code> 本身扩容机制也会导致空间不是刚刚好用完的，而初始化的容量往往结合业务场景给的一个均值，这是很好的。</p><p><code>append</code> 和 <code>copy</code> 在预先确定长度和容量时 <code>append</code> 效果更好一些，主要原因是 <code>copy</code> 需要一个变量来记录位置。 如果使用场景中没有强制限定长度，建议使用 <code>append</code> 因为 <code>append</code> 会根据实际情况再做内存分配，较 <code>copy</code> 也更加灵活一些， 而 <code>copy</code> 往往用在长度固定的地方，可以防止数据长度溢出的问题，例如标准库中 <code>strings.Repeat</code> 函数，它采用指数增长的方式快速填充指定数量的字符，但是如果使用 <code>append</code> 就会发生多余的内存分配，导致长度溢出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Repeat</span><span class="params">(s <span class="keyword">string</span>, count <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">b := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="built_in">len</span>(s)*count)</span><br><span class="line">bp := <span class="built_in">copy</span>(b, s)</span><br><span class="line"><span class="keyword">for</span> bp &lt; <span class="built_in">len</span>(b) &#123;</span><br><span class="line"><span class="built_in">copy</span>(b[bp:], b[:bp])</span><br><span class="line">bp *= <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">string</span>(b)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="csv-reader-中的一个例子"><a href="#csv-reader-中的一个例子" class="headerlink" title="csv reader 中的一个例子"></a>csv reader 中的一个例子</h2><p>官方标准库 <code>csv</code> 的读取性能极高，其中 <code>reader</code> 里面有使用 <code>slice</code> 极好的例子，以下是简略的代码，如果想要全面了解程序需要去看标准库的源码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *Reader)</span> <span class="title">readRecord</span><span class="params">(dst []<span class="keyword">string</span>)</span> <span class="params">([]<span class="keyword">string</span>, error)</span></span> &#123;</span><br><span class="line">line, errRead = r.readLine()</span><br><span class="line"><span class="keyword">if</span> errRead == io.EOF &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, errRead</span><br><span class="line">&#125;</span><br><span class="line">r.recordBuffer = r.recordBuffer[:<span class="number">0</span>]</span><br><span class="line">r.fieldIndexes = r.fieldIndexes[:<span class="number">0</span>]</span><br><span class="line">parseField:</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line"><span class="keyword">if</span> r.TrimLeadingSpace &#123;</span><br><span class="line">line = bytes.TrimLeftFunc(line, unicode.IsSpace)</span><br><span class="line">&#125;</span><br><span class="line">i := bytes.IndexRune(line, r.Comma)</span><br><span class="line">field := line</span><br><span class="line"><span class="keyword">if</span> i &gt;= <span class="number">0</span> &#123;</span><br><span class="line">field = field[:i]</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">field = field[:<span class="built_in">len</span>(field)-lengthNL(field)]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">r.recordBuffer = <span class="built_in">append</span>(r.recordBuffer, field...)</span><br><span class="line">r.fieldIndexes = <span class="built_in">append</span>(r.fieldIndexes, <span class="built_in">len</span>(r.recordBuffer))</span><br><span class="line"><span class="keyword">if</span> i &gt;= <span class="number">0</span> &#123;</span><br><span class="line">line = line[i+commaLen:]</span><br><span class="line"><span class="keyword">continue</span> parseField</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">break</span> parseField</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">err = errRead</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Create a single string and create slices out of it.</span></span><br><span class="line"><span class="comment">// This pins the memory of the fields together, but allocates once.</span></span><br><span class="line">str := <span class="keyword">string</span>(r.recordBuffer) <span class="comment">// Convert to string once to batch allocations</span></span><br><span class="line">dst = dst[:<span class="number">0</span>]</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">cap</span>(dst) &lt; <span class="built_in">len</span>(r.fieldIndexes) &#123;</span><br><span class="line">dst = <span class="built_in">make</span>([]<span class="keyword">string</span>, <span class="built_in">len</span>(r.fieldIndexes))</span><br><span class="line">&#125;</span><br><span class="line">dst = dst[:<span class="built_in">len</span>(r.fieldIndexes)]</span><br><span class="line"><span class="keyword">var</span> preIdx <span class="keyword">int</span></span><br><span class="line"><span class="keyword">for</span> i, idx := <span class="keyword">range</span> r.fieldIndexes &#123;</span><br><span class="line">dst[i] = str[preIdx:idx]</span><br><span class="line">preIdx = idx</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> dst, err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里删除了极多的代码，但是能看懂大意，其中 <code>line</code> 是一段 <code>bufio</code> 中的一段引用，所以这块数据不能返回给用户，也不能进行并发读取操作。</p><p><code>r.recordBuffer</code> 和 <code>r.fieldIndexes</code> 是 <code>csv</code> 的缓存，他们初始的时候容量是0，是不是会有些奇怪，之前还建议 <code>slice</code> 初始一个长度，来减少内存分配，<code>csv</code> 这个库的设计非常的巧妙，假设 <code>csv</code> 每行字段的个数一样，数据长度也相近，现实业务确实如此，所以只有读取第一行数据的时候才会发生大量的 <code>slice</code> 扩容， 之后其它行扩容的可能性非常的小，整个文件读取完也不会发生太多次，不得不说设计的太妙了。</p><p><code>r.recordBuffer</code> 用来存储行中除了分隔符的所有数据，<code>r.fieldIndexes</code> 用来存储每个字段数据在 <code>r.recordBuffer</code> 中的索引。每次都通过 <code>r.recordBuffer[:0]</code> 这个的数据获取，读取每行数据都反复使用这块内存，极大的减少内存开销。</p><p>更巧妙的设计是 <code>str := string(r.recordBuffer) </code> 源代码中也有详细的说明，一次性分配足够的内存， 要知道类型转换是会发生内存拷贝的，分配新的内存， 如果每个字段转换一次，会发生很多的内存拷贝和分配，之后通过 <code>dst[i] = str[preIdx:idx]</code> 引用 <code>str</code> 中的数据达到切分字段的效果，因为引用字符串并不会拷贝字符串（字符串不可变，引用字符串的子串是安全的）所以其代价非常的小。</p><p>这段源码中还有一个很多人都不知道的 <code>slice</code> 特性的例子，<code>dst = dst[:0]; dst = dst[:len(r.fieldIndexes)]</code> 这两句话放到一起是不是感觉很不可思议，明明 <code>dst</code> 的长度被清空了，<code>dst[:len(r.fieldIndexes)]</code> 不是会发生索引越界吗，很多人认为 <code>s[i:l]</code> 这种写法是当前 <code>slice</code> 的索引，实际并非如此，这里面的 <code>i</code> 和 <code>j</code> 是底层引用数组相对当前 <code>slice</code> 引用位置的索引，并不受当前 <code>slice</code> 的长度的影响。</p><p>这里只是简单引用 <code>csv</code> 源码中的一段分析其 <code>slice</code> 的巧妙用法，即把 <code>slice</code> 当做数据缓存，也作为分配内存的一种极佳的方法，这个示例中的关于 <code>slice</code> 的使用值得反复推敲。</p><h2 id="内存池"><a href="#内存池" class="headerlink" title="内存池"></a>内存池</h2><p>早些时间阅读 GitHub 上的一些源码，发现一个实现内存次的例子，里面对 <code>slice</code> 的应用非常有特点，在这里拿来分析一下（<a href="https://github.com/funny/slab/blob/master/chan_pool.go">GitHub源码</a>）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewChanPool</span><span class="params">(minSize, maxSize, factor, pageSize <span class="keyword">int</span>)</span> *<span class="title">ChanPool</span></span> &#123;</span><br><span class="line">pool := &amp;ChanPool&#123;<span class="built_in">make</span>([]chanClass, <span class="number">0</span>, <span class="number">10</span>), minSize, maxSize&#125;</span><br><span class="line"><span class="keyword">for</span> chunkSize := minSize; chunkSize &lt;= maxSize &amp;&amp; chunkSize &lt;= pageSize; chunkSize *= factor &#123;</span><br><span class="line">c := chanClass&#123;</span><br><span class="line">size:   chunkSize,</span><br><span class="line">page:   <span class="built_in">make</span>([]<span class="keyword">byte</span>, pageSize),</span><br><span class="line">chunks: <span class="built_in">make</span>(<span class="keyword">chan</span> []<span class="keyword">byte</span>, pageSize/chunkSize),</span><br><span class="line">&#125;</span><br><span class="line">c.pageBegin = <span class="keyword">uintptr</span>(unsafe.Pointer(&amp;c.page[<span class="number">0</span>]))</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; pageSize/chunkSize; i++ &#123;</span><br><span class="line"><span class="comment">// lock down the capacity to protect append operation</span></span><br><span class="line">mem := c.page[i*chunkSize : (i+<span class="number">1</span>)*chunkSize : (i+<span class="number">1</span>)*chunkSize]</span><br><span class="line">c.chunks &lt;- mem</span><br><span class="line"><span class="keyword">if</span> i == <span class="built_in">len</span>(c.chunks)<span class="number">-1</span> &#123;</span><br><span class="line">c.pageEnd = <span class="keyword">uintptr</span>(unsafe.Pointer(&amp;mem[<span class="number">0</span>]))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">pool.classes = <span class="built_in">append</span>(pool.classes, c)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> pool</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里采用步进式分页，保证每页上的数据块大小相同，一次性创建整个页 <code>make([]byte, pageSize)</code> ，之后从页切分数据块 <code>mem := c.page[i*chunkSize : (i+1)*chunkSize : (i+1)*chunkSize]</code>， 容量和数据块长度一致，创建一块较大的内存，减少系统调用，当然这个例子中还可以创建更大的内存，就是每页容量的总大小，避免创建更多页，所有的块数据都引用一块内存。</p><p>这里限制了每个块的容量，默认引用 <code>slice</code> 的容量是引用起始位置到底层数组的结尾，但是可以指定容量，这就保证了获取的数据块不会因为用户不遵守约定超出其大小导致数据写入到其它块中的问题，设定了容量用户使用超出容量后就会拷贝出去并创建新的 <code>slice</code> 实在的很妙的用法。</p><p>一次分配更大的内存可以减少内存碎片，更好的复用内存。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(pool *ChanPool)</span> <span class="title">Alloc</span><span class="params">(size <span class="keyword">int</span>)</span> []<span class="title">byte</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> size &lt;= pool.maxSize &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(pool.classes); i++ &#123;</span><br><span class="line"><span class="keyword">if</span> pool.classes[i].size &gt;= size &#123;</span><br><span class="line">mem := pool.classes[i].Pop()</span><br><span class="line"><span class="keyword">if</span> mem != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> mem[:size]</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">make</span>([]<span class="keyword">byte</span>, size)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>获取内存池中的内存就非常简单，查找比需要大小更大的块并返回即可，这不失为一个较好的内存复用算法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(pool *ChanPool)</span> <span class="title">Free</span><span class="params">(mem []<span class="keyword">byte</span>)</span></span> &#123;</span><br><span class="line">size := <span class="built_in">cap</span>(mem)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(pool.classes); i++ &#123;</span><br><span class="line"><span class="keyword">if</span> pool.classes[i].size == size &#123;</span><br><span class="line">pool.classes[i].Push(mem)</span><br><span class="line"><span class="keyword">break</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当使用完释放内存时实现的并不是很好，应该判断释放的数据是否是当前内存的一部分，如果不是的就不能放回到内存池中，因为用户未按约定大小使用，导致大量扩容而使得内存池中的数据碎片化，当然用户一旦发生扩容就会导致内存池中的缓存块丢失，导致存在大块内存无法释放，却也没法使用的情况。</p><p>之所以分析这个例子主要是分析其使用 <code>slice</code> 的方法和技巧，并不推荐使用该方法管理内存。</p><h2 id="拓展"><a href="#拓展" class="headerlink" title="拓展"></a>拓展</h2><p>更多关于 <code>slice</code> 应用的例子可以参考标准库 <code>bytes</code> 与 <code>bufio</code>， <code>buffer</code> 与 <code>bufio</code> 的使用极其相似，两个包都是使用 <code>slice</code> 来减少内存分配及系统调用来达到实现缓冲和缓存的例子。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;code&gt;slice&lt;/code&gt; 是 &lt;code&gt;Go&lt;/code&gt; 语言十分重要的数据类型，它承载着很多使命，从语言层面来看是 &lt;code&gt;Go&lt;/code&gt; 语言的内置数据类型，从数据结构来看是动态长度的顺序链表，由于 &lt;code&gt;Go&lt;/code&gt; 不能直接操作内存（通过系统调用可以实现，但是语言本身并不支持），往往 &lt;code&gt;slice&lt;/code&gt; 也可以用来帮助开发者申请大块内存实现缓冲、缓存等功能。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Go&lt;/code&gt; 语言项目中大量的使用 &lt;code&gt;slice&lt;/code&gt;, 我总结三年来对 &lt;code&gt;slice&lt;/code&gt; 的一些操作技巧，以方便可以高效的使用 &lt;code&gt;slice&lt;/code&gt;， 并使用 &lt;code&gt;slice&lt;/code&gt; 解决一些棘手的问题。&lt;/p&gt;
&lt;h2 id=&quot;slice-的基本操作&quot;&gt;&lt;a href=&quot;#slice-的基本操作&quot; class=&quot;headerlink&quot; title=&quot;slice 的基本操作&quot;&gt;&lt;/a&gt;slice 的基本操作&lt;/h2&gt;&lt;p&gt;先熟悉一些 &lt;code&gt;slice&lt;/code&gt; 的基本的操作, 对最常规的 &lt;code&gt;:&lt;/code&gt; 操作就可玩出很多花样。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s=ss[:]&lt;/code&gt; 引用一个切片或数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s=s[:0]&lt;/code&gt; 清空切片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s=s[:10]&lt;/code&gt; &lt;code&gt;s=s[10:]&lt;/code&gt; &lt;code&gt;s=s[10:20]&lt;/code&gt; 截取接片&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s=ss[0:10:20]&lt;/code&gt; 从切片或数组引用指定长度和容量的切片&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下标索引操作的一些误区 &lt;code&gt;s[i:l:c]&lt;/code&gt; &lt;code&gt;i&lt;/code&gt; 是起始偏移的起始位置，&lt;code&gt;l&lt;/code&gt; 是起始偏移的长度结束位置， &lt;code&gt;l-i&lt;/code&gt; 就是新 &lt;code&gt;slice&lt;/code&gt; 的长度， &lt;code&gt;c&lt;/code&gt; 是起始偏移的容量结束位置，&lt;code&gt;c-i&lt;/code&gt; 就是新 &lt;code&gt;slice&lt;/code&gt; 的容量。其中 &lt;code&gt;i&lt;/code&gt; 、&lt;code&gt;l&lt;/code&gt; 、&lt;code&gt;c&lt;/code&gt; 并不是当前 &lt;code&gt;slice&lt;/code&gt; 的索引，而是引用底层数组相对当前 &lt;code&gt;slice&lt;/code&gt; 起始位置的偏移量，所以是可超出当前 &lt;code&gt;slice&lt;/code&gt; 的长度的， 但不能超出当前 &lt;code&gt;slice&lt;/code&gt; 的容量，如下操作是合法的：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;package&lt;/span&gt; main&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; (&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;string&quot;&gt;&amp;quot;fmt&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;()&lt;/span&gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	s := &lt;span class=&quot;built_in&quot;&gt;make&lt;/span&gt;([]&lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;, &lt;span class=&quot;number&quot;&gt;100&lt;/span&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	s[&lt;span class=&quot;number&quot;&gt;20&lt;/span&gt;] = &lt;span class=&quot;number&quot;&gt;100&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	s1 := s[&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;:&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	s2 := s1[&lt;span class=&quot;number&quot;&gt;10&lt;/span&gt;:&lt;span class=&quot;number&quot;&gt;20&lt;/span&gt;]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	fmt.Println(s1)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	fmt.Println(s2)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;其中 &lt;code&gt;s1&lt;/code&gt; 是 &lt;code&gt;[]&lt;/code&gt;；&lt;code&gt;s2&lt;/code&gt; 是 &lt;code&gt;[100 0 0 0 0 0 0 0 0 0]&lt;/code&gt;, 这里并不会发生下标越界的情况，一个更好的例子在 &lt;a href=&quot;#csv-reader-%E4%B8%AD%E7%9A%84%E4%B8%80%E4%B8%AA%E4%BE%8B%E5%AD%90&quot;&gt;csv reader 中的一个例子&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/tags/go/"/>
    
    <category term="slice" scheme="https://blog.thinkeridea.com/tags/slice/"/>
    
    <category term="缓存" scheme="https://blog.thinkeridea.com/tags/%E7%BC%93%E5%AD%98/"/>
    
    <category term="内存池" scheme="https://blog.thinkeridea.com/tags/%E5%86%85%E5%AD%98%E6%B1%A0/"/>
    
  </entry>
  
  <entry>
    <title>【Go】深入剖析slice和array</title>
    <link href="https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html"/>
    <id>https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html</id>
    <published>2019-01-11T22:36:26.000Z</published>
    <updated>2019-02-24T12:53:45.782Z</updated>
    
    <content type="html"><![CDATA[<p><code>array</code> 和 <code>slice</code> 看似相似，却有着极大的不同，但他们之间还有着千次万缕的联系 <code>slice</code> 是引用类型、是 <code>array</code> 的引用，相当于动态数组，<br>这些都是 <code>slice</code> 的特性，但是 <code>slice</code> 底层如何表现，内存中是如何分配的，特别是在程序中大量使用 <code>slice</code> 的情况下，怎样可以高效使用 <code>slice</code>？<br>今天借助 <code>Go</code> 的 <code>unsafe</code> 包来探索 <code>array</code> 和 <code>slice</code> 的各种奥妙。</p><a id="more"></a><h2 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h2><p><code>slice</code> 是在 <code>array</code> 的基础上实现的，需要先详细了解一下数组。</p><p>** 维基上如此介绍数组：**</p><blockquote><p>在计算机科学中，数组数据结构（英语：array data structure），简称数组（英语：Array），是由相同类型的元素（element）的集合所组成的数据结构，分配一块连续的内存来存储，利用元素的索引（index）可以计算出该元素对应的存储地址。<br>** 数组设计之初是在形式上依赖内存分配而成的，所以必须在使用前预先请求空间。这使得数组有以下特性：**</p><ol><li>请求空间以后大小固定，不能再改变（数据溢出问题）；</li><li>在内存中有空间连续性的表现，中间不会存在其他程序需要调用的数据，为此数组的专用内存空间；</li><li>在旧式编程语言中（如有中阶语言之称的C），程序不会对数组的操作做下界判断，也就有潜在的越界操作的风险（比如会把数据写在运行中程序需要调用的核心部分的内存上）。</li></ol></blockquote><p>根据维基的介绍，了解到数组是存储在一段连续的内存中，每个元素的类型相同，即是每个元素的宽度相同，可以根据元素的宽度计算元素存储的位置。<br>通过这段介绍总结一下数组有一下特性：</p><ul><li>分配在连续的内存地址上</li><li>元素类型一致，元素存储宽度一致</li><li>空间大小固定，不能修改</li><li>可以通过索引计算出元素对应存储的位置（只需要知道数组内存的起始位置和数据元素宽度即可）</li><li>会出现数据溢出的问题（下标越界）</li></ul><p><code>Go</code> 中的数组如何实现的呢，恰恰就是这么实现的，实际上几乎所有计算机语言，数组的实现都是相似的，也拥有上面总结的特性。<br><code>Go</code> 语言的数组不同于 <code>C</code> 语言或者其他语言的数组，<code>C</code> 语言的数组变量是指向数组第一个元素的指针；<br>而 <code>Go</code> 语言的数组是一个值，<code>Go</code> 语言中的数组是值类型，一个数组变量就表示着整个数组，意味着 <code>Go</code> 语言的数组在传递的时候，传递的是原数组的拷贝。</p><p>在程序中数组的初始化有两种方法 <code>arr := [10]int&#123;&#125;</code> 或 <code>var arr  [10]int</code>，但是不能使用 <code>make</code> 来创建，数组这节结束时再探讨一下这个问题。<br>使用 <code>unsafe</code>来看一下在内存中都是如何存储的吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> arr = [<span class="number">3</span>]<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line"></span><br><span class="line">fmt.Println(unsafe.Sizeof(arr))</span><br><span class="line">size := unsafe.Sizeof(arr[<span class="number">0</span>])</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取数组指定索引元素的值</span></span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;arr[<span class="number">0</span>])) + <span class="number">1</span>*size)))</span><br><span class="line"></span><br><span class="line"><span class="comment">// 设置数组指定索引元素的值</span></span><br><span class="line">*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;arr[<span class="number">0</span>])) + <span class="number">1</span>*size)) = <span class="number">10</span></span><br><span class="line"></span><br><span class="line">fmt.Println(arr[<span class="number">1</span>])</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码的输出如下 (<a href="https://play.golang.org/p/3v-rAQZG-E6">Go Playground</a>)：</p><blockquote><p>12<br>2<br>10</p></blockquote><p>首先说 <code>12</code> 是 <code>fmt.Println(unsafe.Sizeof(arr))</code> 输出的，<code>unsafe.Sizeof</code> 用来计算当前变量的值在内存中的大小，<code>12</code> 这个代表一个 <code>int</code> 有4个字节，<code>3 * 4</code> 就是 <code>12</code>。<br>这是在32位平台上运行得出的结果， 如果在64位平台上运行数组的大小是 <code>24</code>。从这里可以看出 <code>[3]int</code> 在内存中由3个连续的 <code>int</code> 类型组成，且有 <code>12</code> 个字节那么长，这就说明了数组在内存中没有存储多余的数据，只存储元素本身。</p><p><code>size := unsafe.Sizeof(arr[0])</code> 用来计算单个元素的宽度，<code>int</code>在32位平台上就是4个字节，<code>uintptr(unsafe.Pointer(&amp;arr[0]))</code> 用来计算数组起始位置的指针，<code>1*size</code> 用来获取索引为1的元素相对数组起始位置的偏移，<code>unsafe.Pointer(uintptr(unsafe.Pointer(&amp;arr[0])) + 1*size))</code> 获取索引为1的元素指针，<code>*(*int)</code> 用来转换指针位置的数据类型， 因为 <code>int</code> 是4个字节，所以只会读取4个字节的数据，由元素类型限制数据宽度，来确定元素的结束位置，因此得到的结果是 <code>2</code>。</p><p>上一个步骤获取元素的值，其中先获取了元素的指针，赋值的时候只需要对这个指针位置设置值就可以了， <code>*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&amp;arr[0])) + 1*size)) = 10</code> 就是用来给指定下标元素赋值。</p><p><img src="/assets/image/20190111/array.jpg" alt="数组在内存中的结构"></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">n:= <span class="number">10</span></span><br><span class="line"><span class="keyword">var</span> arr = [n]<span class="keyword">int</span>&#123;&#125;</span><br><span class="line">fmt.Println(arr)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如上代码，动态的给数组设定长度，会导致编译错误 <code>non-constant array bound n</code>， 由此推导数组的所有操作都是编译时完成的，会转成对应的指令，通过这个特性知道数组的长度是数组类型不可或缺的一部分，并且必须在编写程序时确定。<br>可以通过 <code>GOOS=linux GOARCH=amd64 go tool compile -S array.go</code> 来获取对应的汇编代码，在 <code>array.go</code> 中做一些数组相关的操作，查看转换对应的指令。</p><p>之前的疑问，为什么数组不能用 <code>make</code> 创建？ 上面分析了解到数组操作是在编译时转换成对应指令的，而 <code>make</code> 是在运行时处理（特殊状态下会做编译器优化，make可以被优化，下面 <code>slice</code> 分析时来讲）。</p><h2 id="slice"><a href="#slice" class="headerlink" title="slice"></a>slice</h2><p>因为数组是固定长度且是值传递，很不灵活，所以在 <code>Go</code> 程序中很少看到数组的影子。然而 <code>slice</code> 无处不在，<code>slice</code> 以数组为基础，提供强大的功能和遍历性。<br><code>slice</code> 的类型规范是[]T，<code>slice</code> T元素的类型。与数组类型不同，<code>slice</code> 类型没有指定的长度。</p><p>** <code>slice</code> 申明的几种方法：** </p><blockquote><p><code>s := []int&#123;1, 2, 3&#125;</code> 简短的赋值语句<br><code>var s []int</code> <code>var</code> 申明<br><code>make([]int, 3, 8)</code> 或 <code>make([]int, 3)</code> <code>make</code> 内置方法创建<br><code>s := ss[:5] </code> 从切片或者数组创建</p></blockquote><p>** <code>slice</code> 有两个内置函数来获取其属性：**</p><blockquote><p><code>len</code> 获取 <code>slice</code> 的长度<br><code>cap</code> 获取 <code>slice</code> 的容量</p></blockquote><p><code>slice</code> 的属性，这东西是什么，还需借助 <code>unsafe</code> 来探究一下。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">10</span>, <span class="number">20</span>)</span><br><span class="line"></span><br><span class="line">s[<span class="number">2</span>] = <span class="number">100</span></span><br><span class="line">s[<span class="number">9</span>] = <span class="number">200</span></span><br><span class="line"></span><br><span class="line">size := unsafe.Sizeof(<span class="number">0</span>)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Println(*(*[<span class="number">20</span>]<span class="keyword">int</span>)(unsafe.Pointer(*(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码的输出如下 (<a href="https://play.golang.org/p/Z_TMWD53syD">Go Playground</a>)：</p><blockquote><p>c00007ce90<br>10<br>20<br>[0 0 100 0 0 0 0 0 0 200 0 0 0 0 0 0 0 0 0 0]</p></blockquote><p>这段输出除了第一个，剩余三个好像都能看出点什么， <code>10</code> 不是创建 <code>slice</code> 的长度吗，<code>20</code> 不就是指定的容量吗， 最后这个看起来有点像 <code>slice</code> 里面的数据，但是数量貌似有点多，从第三个元素和第十个元素来看，正好是给 <code>slice</code> 索引 <code>2</code> 和 <code>10</code> 指定的值，但是切片不是长度是 <code>10</code> 个吗，难道这个是容量，容量刚好是 <code>20</code>个。 </p><p>第二和第三个输出很好弄明白，就是 <code>slice</code> 的长度和容量， 最后一个其实是 <code>slice</code> 引用底层数组的数据，因为创建容量为 <code>20</code>，所以底层数组的长度就是 <code>20</code>，从这里了解到切片是引用底层数组上的一段数据，底层数组的长度就是 <code>slice</code> 的容量，由于数组长度不可变的特性，当 <code>slice</code> 的长度达到容量大小之后就需要考虑扩容，不是说数组长度不能变吗，那 <code>slice</code> 怎么实现扩容呢， 其实就是在内存上分配一个更大的数组，把当前数组上的内容拷贝到新的数组上， <code>slice</code> 来引用新的数组，这样就实现扩容了。</p><p>说了这么多，还是没有看出来 <code>slice</code> 是如何引用数组的，额…… 之前的程序还有一个输出没有搞懂是什么，难道这个就是底层数组的引用。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">arr := [<span class="number">10</span>]<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">arr[<span class="number">7</span>] = <span class="number">100</span></span><br><span class="line">arr[<span class="number">9</span>] = <span class="number">200</span></span><br><span class="line"></span><br><span class="line">fmt.Println(arr)</span><br><span class="line"></span><br><span class="line">s1 := arr[:]</span><br><span class="line">s2 := arr[<span class="number">2</span>:<span class="number">8</span>]</span><br><span class="line"></span><br><span class="line">size := unsafe.Sizeof(<span class="number">0</span>)</span><br><span class="line">fmt.Println(<span class="string">&quot;----------s1---------&quot;</span>)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s1)))</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, <span class="keyword">uintptr</span>(unsafe.Pointer(&amp;arr[<span class="number">0</span>])))</span><br><span class="line"></span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Println(s1)</span><br><span class="line">fmt.Println(*(*[<span class="number">10</span>]<span class="keyword">int</span>)(unsafe.Pointer(*(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s1)))))</span><br><span class="line"></span><br><span class="line">fmt.Println(<span class="string">&quot;----------s2---------&quot;</span>)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s2)))</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, <span class="keyword">uintptr</span>(unsafe.Pointer(&amp;arr[<span class="number">0</span>]))+size*<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s2)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s2)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Println(s2)</span><br><span class="line">fmt.Println(*(*[<span class="number">8</span>]<span class="keyword">int</span>)(unsafe.Pointer(*(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s2)))))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码输出如下(<a href="https://play.golang.org/p/4KgHvKClbzZ">Go Playground</a>)：</p><blockquote><p>[1 2 3 0 0 0 0 100 0 200]<br>  ———-s1———<br>  c00001c0a0<br>  c00001c0a0<br>  10<br>  10<br>  [1 2 3 0 0 0 0 100 0 200]<br>  [1 2 3 0 0 0 0 100 0 200]<br>  ———-s2———<br>  c00001c0b0<br>  c00001c0b0<br>  6<br>  8<br>  [3 0 0 0 0 100]<br>[3 0 0 0 0 100 0 200]</p></blockquote><p>这段输出看起来有点小复杂，第一行输出就不用说了吧，这个是打印整个数组的数据。先分析一下 <code>s1</code> 变量的下面的输出吧，<code>s1 := arr[:]</code> 引用了整个数组，所以在第5、6行输出都是10，因为数组长度为10，所有 <code>s1</code> 的长度和容量都为10，那第3、4行输出是什么呢，他们怎么都一样呢，之前分析数组的时候 通过 <code>uintptr(unsafe.Pointer(&amp;arr[0]))</code> 来获取数组起始位置的指针的，那么第4行打印的就是数组的指针，这么就了解了第三行输出的是上面了吧，就是数组起始位置的指针，所以 <code>*(*uintptr)(unsafe.Pointer(&amp;s1))</code> 获取的就是引用数组的指针，但是这个并不是数组起始位置的指针，而是 <code>slice</code> 引用数组元素的指针，为什么这么说呢？</p><p>接着看 <code>s2</code> 变量下面的输出吧，<code>s2 := arr[2:8]</code> 引用数组第3~8的元素，那么 <code>s2</code> 的长度就是 6。 根据经验可以知道 <code>s2</code> 变量输出下面第3行就是 <code>slice</code> 的长度，但是为啥第4行是 <code>8</code> 呢，<code>slice</code> 应用数组的指定索引起始位置到数组结尾就是 <code>slice</code> 的容量， 所以 所以从第3个位置到末尾，就是8个容量。在看第1行和第2行的输出，之前分析数组的时候通过 <code>uintptr(unsafe.Pointer(&amp;arr[0]))+size*2</code> 来获取数组指定索引位置的指针，那么这段第2行就是数组索引为2的元素指针，<code>*(*uintptr)(unsafe.Pointer(&amp;s2))</code> 是获取切片的指针，第1行和第2行输出一致，所以 <code>slice</code> 实际是引用数组元素位置的指针，并不是数组起始位置的指针。</p><p>** 总结：**</p><ul><li><code>slice</code> 是的起始位置是引用数组元素位置的指针。</li><li><code>slice</code> 的长度是引用数组元素起始位置到结束位置的长度。</li><li><code>slice</code> 的容量是引用数组元素起始位置到数组末尾的长度。</li></ul><p>经过上面一轮分析了解到 <code>slice</code> 有三个属性，引用数组元素位置指针、长度和容量。实际上 <code>slice</code> 的结构像下图一样：</p><p><img src="/assets/image/20190111/slice_1.jpg" alt="slice"></p><h2 id="slice-增长"><a href="#slice-增长" class="headerlink" title="slice 增长"></a>slice 增长</h2><p><code>slice</code> 是如何增长的，用 <code>unsafe</code> 分析一下看看：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">9</span>, <span class="number">10</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 引用底层的数组地址</span></span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line"></span><br><span class="line">s = <span class="built_in">append</span>(s, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 引用底层的数组地址</span></span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line"></span><br><span class="line">s = <span class="built_in">append</span>(s, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 引用底层的数组地址</span></span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码的输出(<a href="https://play.golang.org/p/3c4ek4-0ft5">Go Playground</a>):</p><blockquote><p>c000082e90<br>  9 10<br>  c000082e90<br>  10 10<br>  c00009a000<br>11 20</p></blockquote><p>从结果上看前两次地址是一样的，初始化一个长度为9，容量为10的 <code>slice</code>，当第一次 <code>append</code> 的时候容量是足够的，所以底层引用数组地址未发生变化，此时 <code>slice</code> 的长度和容量都为10，之后再次 <code>append</code> 的时候发现底层数组的地址不一样了，因为 <code>slice</code> 的长度超过了容量，但是新的 <code>slice</code> 容量并不是11而是20，这要说 <code>slice</code> 的机制了，因为数组长度不可变，想扩容 <code>slice</code>就必须分配一个更大的数组，并把之前的数据拷贝到新数组，如果一次只增加1个长度，那就会那发生大量的内存分配和数据拷贝，这个成本是很大的，所以 <code>slice</code> 是有一个增长策略的。</p><p><code>Go</code> 标准库 <code>runtime/slice.go</code> 当中有详细的 <code>slice</code> 增长策略的逻辑：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">growslice</span><span class="params">(et *_type, old slice, <span class="built_in">cap</span> <span class="keyword">int</span>)</span> <span class="title">slice</span></span> &#123;</span><br><span class="line">    .....</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 计算新的容量，核心算法用来决定slice容量增长</span></span><br><span class="line">newcap := old.<span class="built_in">cap</span></span><br><span class="line">doublecap := newcap + newcap</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">cap</span> &gt; doublecap &#123;</span><br><span class="line">newcap = <span class="built_in">cap</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">if</span> old.<span class="built_in">len</span> &lt; <span class="number">1024</span> &#123;</span><br><span class="line">newcap = doublecap</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">for</span> <span class="number">0</span> &lt; newcap &amp;&amp; newcap &lt; <span class="built_in">cap</span> &#123;</span><br><span class="line">newcap += newcap / <span class="number">4</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> newcap &lt;= <span class="number">0</span> &#123;</span><br><span class="line">newcap = <span class="built_in">cap</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 根据et.size调整新的容量</span></span><br><span class="line"><span class="keyword">var</span> overflow <span class="keyword">bool</span></span><br><span class="line"><span class="keyword">var</span> lenmem, newlenmem, capmem <span class="keyword">uintptr</span></span><br><span class="line"><span class="keyword">switch</span> &#123;</span><br><span class="line"><span class="keyword">case</span> et.size == <span class="number">1</span>:</span><br><span class="line">lenmem = <span class="keyword">uintptr</span>(old.<span class="built_in">len</span>)</span><br><span class="line">newlenmem = <span class="keyword">uintptr</span>(<span class="built_in">cap</span>)</span><br><span class="line">capmem = roundupsize(<span class="keyword">uintptr</span>(newcap))</span><br><span class="line">overflow = <span class="keyword">uintptr</span>(newcap) &gt; maxAlloc</span><br><span class="line">newcap = <span class="keyword">int</span>(capmem)</span><br><span class="line"><span class="keyword">case</span> et.size == sys.PtrSize:</span><br><span class="line">lenmem = <span class="keyword">uintptr</span>(old.<span class="built_in">len</span>) * sys.PtrSize</span><br><span class="line">newlenmem = <span class="keyword">uintptr</span>(<span class="built_in">cap</span>) * sys.PtrSize</span><br><span class="line">capmem = roundupsize(<span class="keyword">uintptr</span>(newcap) * sys.PtrSize)</span><br><span class="line">overflow = <span class="keyword">uintptr</span>(newcap) &gt; maxAlloc/sys.PtrSize</span><br><span class="line">newcap = <span class="keyword">int</span>(capmem / sys.PtrSize)</span><br><span class="line"><span class="keyword">case</span> isPowerOfTwo(et.size):</span><br><span class="line"><span class="keyword">var</span> shift <span class="keyword">uintptr</span></span><br><span class="line"><span class="keyword">if</span> sys.PtrSize == <span class="number">8</span> &#123;</span><br><span class="line"><span class="comment">// Mask shift for better code generation.</span></span><br><span class="line">shift = <span class="keyword">uintptr</span>(sys.Ctz64(<span class="keyword">uint64</span>(et.size))) &amp; <span class="number">63</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">shift = <span class="keyword">uintptr</span>(sys.Ctz32(<span class="keyword">uint32</span>(et.size))) &amp; <span class="number">31</span></span><br><span class="line">&#125;</span><br><span class="line">lenmem = <span class="keyword">uintptr</span>(old.<span class="built_in">len</span>) &lt;&lt; shift</span><br><span class="line">newlenmem = <span class="keyword">uintptr</span>(<span class="built_in">cap</span>) &lt;&lt; shift</span><br><span class="line">capmem = roundupsize(<span class="keyword">uintptr</span>(newcap) &lt;&lt; shift)</span><br><span class="line">overflow = <span class="keyword">uintptr</span>(newcap) &gt; (maxAlloc &gt;&gt; shift)</span><br><span class="line">newcap = <span class="keyword">int</span>(capmem &gt;&gt; shift)</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">lenmem = <span class="keyword">uintptr</span>(old.<span class="built_in">len</span>) * et.size</span><br><span class="line">newlenmem = <span class="keyword">uintptr</span>(<span class="built_in">cap</span>) * et.size</span><br><span class="line">capmem = roundupsize(<span class="keyword">uintptr</span>(newcap) * et.size)</span><br><span class="line">overflow = <span class="keyword">uintptr</span>(newcap) &gt; maxSliceCap(et.size)</span><br><span class="line">newcap = <span class="keyword">int</span>(capmem / et.size)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">    ......</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> p unsafe.Pointer</span><br><span class="line"><span class="keyword">if</span> et.kind&amp;kindNoPointers != <span class="number">0</span> &#123;</span><br><span class="line">p = mallocgc(capmem, <span class="literal">nil</span>, <span class="literal">false</span>)  <span class="comment">// 分配新的内存</span></span><br><span class="line">memmove(p, old.array, lenmem) <span class="comment">// 拷贝数据</span></span><br><span class="line">memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">p = mallocgc(capmem, et, <span class="literal">true</span>) <span class="comment">// 分配新的内存</span></span><br><span class="line"><span class="keyword">if</span> !writeBarrier.enabled &#123;</span><br><span class="line">memmove(p, old.array, lenmem)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">uintptr</span>(<span class="number">0</span>); i &lt; lenmem; i += et.size &#123;</span><br><span class="line">typedmemmove(et, add(p, i), add(old.array, i)) <span class="comment">// 拷贝数据</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> slice&#123;p, old.<span class="built_in">len</span>, newcap&#125; <span class="comment">// 新slice引用新的数组，长度为旧数组的长度，容量为新数组的容量</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>基本呢就三个步骤，计算新的容量、分配新的数组、拷贝数据到新数组，社区很多人分享 <code>slice</code> 的增长方法，实际都不是很精确，因为大家只分析了计算 <code>newcap</code> 的那一段，也就是上面注释的第一部分，下面的 <code>switch</code> 根据 <code>et.size</code> 来调整 <code>newcap</code> 一段被直接忽略，社区的结论是：”如果 <code>selic</code> 的容量小于1024个元素，那么扩容的时候 <code>slice</code> 的 <code>cap</code> 就翻番，乘以2；一旦元素个数超过1024个元素，增长因子就变成1.25，即每次增加原来容量的四分之一” 大多数情况也确实如此，但是根据 <code>newcap</code> 的计算规则，如果新的容量超过旧的容量2倍时会直接按新的容量分配，真的是这样吗?</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">10</span>, <span class="number">10</span>)</span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s), <span class="built_in">cap</span>(s))</span><br><span class="line">s2 := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">40</span>)</span><br><span class="line"></span><br><span class="line">s = <span class="built_in">append</span>(s, s2...)</span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s), <span class="built_in">cap</span>(s))</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码的输出(<a href="https://play.golang.org/p/x8kN4V5R7YW">Go Playground</a>):</p><blockquote><p>10 10<br>50 52</p></blockquote><p>这个结果有点出人意料， 如果是2倍增长应该是 <code>10 * 2 * 2 * 2</code> 结果应该是80， 如果说新的容量高于旧容量的两倍但结果也不是50，实际上 <code>newcap</code> 的结果就是50，那段逻辑很好理解，但是<code>switch</code> 根据 <code>et.size</code> 来调整 <code>newcap</code> 后就是52了，这段逻辑走到了 <code>case et.size == sys.PtrSize</code> 这段，详细的以后做源码分析再说。</p><p>** 总结 **</p><ul><li>当 <code>slice</code> 的长度超过其容量，会分配新的数组，并把旧数组上的值拷贝到新的数组</li><li>逐个元素添加到 <code>slice</code> 并操过其容量， 如果 <code>selic</code> 的容量小于1024个元素，那么扩容的时候 <code>slice</code> 的 <code>cap</code> 就翻番，乘以2；一旦元素个数超过1024个元素，增长因子就变成1.25，即每次增加原来容量的四分之一。</li><li>批量添加元素，当新的容量高于旧容量的两倍，就会分配比新容量稍大一些，并不会按上面第二条的规则扩容。</li><li>当 <code>slice</code> 发生扩容，引用新数组后，<code>slice</code> 操作不会再影响旧的数组，而是新的数组（社区经常讨论的传递 <code>slice</code> 容量超出后，修改数据不会作用到旧的数据上），所以往往设计函数如果会对长度调整都会返回新的 <code>slice</code>，例如 <code>append</code> 方法。</li></ul><h2 id="slice-是引用类型？"><a href="#slice-是引用类型？" class="headerlink" title="slice 是引用类型？"></a>slice 是引用类型？</h2><p><code>slice</code> 不发生扩容，所有的修改都会作用在原数组上，那如果把 <code>slice</code> 传递给一个函数或者赋值给另一个变量会发生什么呢，<code>slice</code> 是引用类型，会有新的内存被分配吗。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">10</span>, <span class="number">20</span>)</span><br><span class="line"></span><br><span class="line">size := unsafe.Sizeof(<span class="number">0</span>)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%p\n&quot;</span>, &amp;s)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">slice(s)</span><br><span class="line"></span><br><span class="line">s1 := s</span><br><span class="line">fmt.Printf(<span class="string">&quot;%p\n&quot;</span>, &amp;s1)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s1)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;-&quot;</span>, <span class="number">50</span>))</span><br><span class="line"></span><br><span class="line">*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size)) = <span class="number">20</span></span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s1)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s1)) + size*<span class="number">2</span>)))</span><br><span class="line"></span><br><span class="line">fmt.Println(s)</span><br><span class="line">fmt.Println(s1)</span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;-&quot;</span>, <span class="number">50</span>))</span><br><span class="line"></span><br><span class="line">s2 := s</span><br><span class="line">s2 = <span class="built_in">append</span>(s2, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s), <span class="built_in">cap</span>(s), s)</span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s1), <span class="built_in">cap</span>(s1), s1)</span><br><span class="line">fmt.Println(<span class="built_in">len</span>(s2), <span class="built_in">cap</span>(s2), s2)</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">slice</span><span class="params">(s []<span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">size := unsafe.Sizeof(<span class="number">0</span>)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%p\n&quot;</span>, &amp;s)</span><br><span class="line">fmt.Printf(<span class="string">&quot;%x\n&quot;</span>, *(*<span class="keyword">uintptr</span>)(unsafe.Pointer(&amp;s)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size)))</span><br><span class="line">fmt.Println(*(*<span class="keyword">int</span>)(unsafe.Pointer(<span class="keyword">uintptr</span>(unsafe.Pointer(&amp;s)) + size*<span class="number">2</span>)))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子(<a href="https://play.golang.org/p/98NZEDdh0Mm">Go Playground</a>)比较长就不逐一分析了，在这个例子里面调用函数传递 <code>slice</code> 其变量的地址发生了变化， 但是引用数组的地址，<code>slice</code> 的长度和容量都没有变化， 这说明是对 <code>slice</code> 的浅拷贝，拷贝 <code>slice</code> 的三个属性创建一个新的变量，虽然引用底层数组还是一个，但是变量并不是一个。</p><p>第二个创建 <code>s1</code> 变量，使用 <code>s</code> 为其赋值，发现 <code>s1</code> 和函数调用一样也是 <code>s</code> 的浅拷贝，之后修改 <code>s1</code> 的长度发现 <code>s1</code> 的长度发生变化，但是 <code>s</code> 的长度保持不变， 这也说明 <code>s1</code> 就是 <code>s</code> 的浅拷贝。</p><p>这样设计有什么优势呢，第三步创建 <code>s2</code> 变量， 并且 <code>append</code> 一个元素， 发现 <code>s2</code> 的长度发生变化了， <code>s</code> 并没有，虽然这个数据就在底层数组上，但是用常规的方法 <code>s</code> 是看不到第11个位置上的数据的， <code>s1</code> 因为长度覆盖到第11个元素，所有能够看到这个数据的变化。这里能看到采用浅拷贝的方式可以使得切片的属性各自独立，而不会相互影响，这样可以有一定的隔离性，缺点也很明显，如果两个变量都引用同一个数组，同时 <code>append</code>， 在不发生扩容的情况下，总是最后一个 <code>append</code> 的结果被保留，可能引起一些编程上疑惑。</p><p>** 总结 **</p><p><code>slice</code> 是引用类型，但是和 <code>C</code> 传引用是有区别的， <code>C</code> 里面的传引用是在编译器对原变量数据引用， 并不会发生内存分配，而 <code>Go</code> 里面的引用类型传递和赋值会进行浅拷贝，在32位平台上有12个字节的内存分配， 在64位上有24字节的内存分配。</p><p><strong>* 传引用和引用类型是有区别的， <code>slice</code> 是引用类型。*</strong> </p><h2 id="slice-的三种状态"><a href="#slice-的三种状态" class="headerlink" title="slice 的三种状态"></a>slice 的三种状态</h2><p><code>slice</code> 有三种状态：零切片、空切片、nil切片。</p><h4 id="零切片"><a href="#零切片" class="headerlink" title="零切片"></a>零切片</h4><p>所有的类型都有零值，如果 <code>slice</code> 所引用数组元素都没有赋值，就是所有元素都是类型零值，那这就是零切片。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> s = <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">10</span>)</span><br><span class="line">fmt.Println(s)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> s1 = <span class="built_in">make</span>([]*<span class="keyword">int</span>, <span class="number">10</span>)</span><br><span class="line">fmt.Println(s1)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> s2 = <span class="built_in">make</span>([]<span class="keyword">string</span>, <span class="number">10</span>)</span><br><span class="line">fmt.Println(s2)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码输出(<a href="https://play.golang.org/p/RWJv8t6goas">Go Playground</a>):</p><blockquote><p>[0 0 0 0 0 0 0 0 0 0]<br> [<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>]<br>[         ]</p></blockquote><p>零切片很好理解，数组元素都为类型零值即为零切片，这种状态下的 <code>slice</code> 和正常的 <code>slice</code> 操作没有任何区别。</p><h4 id="空切片"><a href="#空切片" class="headerlink" title="空切片"></a>空切片</h4><p>空切片可以理解就是切片的长度为0，就是说 <code>slice</code> 没有元素。 社区大多数解释空切片为引用底层数组为 <code>zerobase</code> 这个特殊的指针。但是从操作上看空切片所有的表现就是切片长度为0，如果容量也为零底层数组就会指向 <code>zerobase</code> ，这样就不会发生内存分配， 如果容量不会零就会指向底层数据，会有内存分配。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;strings&quot;</span></span><br><span class="line"><span class="string">&quot;unsafe&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> s []<span class="keyword">int</span></span><br><span class="line">s1 := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>)</span><br><span class="line">s2 := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line">s3 := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">100</span>)</span><br><span class="line"></span><br><span class="line">arr := [<span class="number">10</span>]<span class="keyword">int</span>&#123;&#125;</span><br><span class="line">s4 := arr[:<span class="number">0</span>]</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;--s--&quot;</span>, <span class="number">10</span>))</span><br><span class="line">fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&amp;s)))</span><br><span class="line">fmt.Println(s)</span><br><span class="line">fmt.Println(s == <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;--s1--&quot;</span>, <span class="number">10</span>))</span><br><span class="line">fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&amp;s1)))</span><br><span class="line">fmt.Println(s1)</span><br><span class="line">fmt.Println(s1 == <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;--s2--&quot;</span>, <span class="number">10</span>))</span><br><span class="line">fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&amp;s2)))</span><br><span class="line">fmt.Println(s2)</span><br><span class="line">fmt.Println(s2 == <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;--s3--&quot;</span>, <span class="number">10</span>))</span><br><span class="line">fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&amp;s3)))</span><br><span class="line">fmt.Println(s3)</span><br><span class="line">fmt.Println(s3 == <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">fmt.Println(strings.Repeat(<span class="string">&quot;--s4--&quot;</span>, <span class="number">10</span>))</span><br><span class="line">fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&amp;s4)))</span><br><span class="line">fmt.Println(s4)</span><br><span class="line">fmt.Println(s4 == <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上代码输出(<a href="https://play.golang.org/p/I145ObQ32yG">Go Playground</a>):</p><blockquote><p>–s—-s—-s—-s—-s—-s—-s—-s—-s—-s–<br> {0 0 0}<br> []<br> –s1—-s1—-s1—-s1—-s1—-s1—-s1—-s1—-s1—-s1–<br> {18349960 0 0}<br> []<br> –s2—-s2—-s2—-s2—-s2—-s2—-s2—-s2—-s2—-s2–<br> {18349960 0 0}<br> []<br> –s3—-s3—-s3—-s3—-s3—-s3—-s3—-s3—-s3—-s3–<br> {824634269696 0 100}<br> []<br> –s4—-s4—-s4—-s4—-s4—-s4—-s4—-s4—-s4—-s4–<br> {824633835680 0 10}<br>[]</p></blockquote><p>以上示例中除了 <code>s</code> 其它的 <code>slice</code> 都是空切片，打印出来全部都是 <code>[]</code>，<code>s</code> 是nil切片下一小节说。要注意 <code>s1</code> 和 <code>s2</code> 的长度和容量都为0，且引用数组指针都是 <code>18349960</code>， 这点太重要了，因为他们都指向 <code>zerobase</code> 这个特殊的指针，是没有内存分配的。</p><p><img src="/assets/image/20190111/slice_3.jpg" alt="slice"></p><h4 id="nil切片"><a href="#nil切片" class="headerlink" title="nil切片"></a>nil切片</h4><p>什么是nil切片，这个名字说明nil切片没有引用任何底层数组，底层数组的地址为nil就是nil切片。上一小节中的 <code>s</code> 就是一个nil切片，它的底层数组指针为0，代表是一个 <code>nil</code> 指针。</p><p><img src="/assets/image/20190111/slice_2.jpg" alt="slice"></p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>零切片就是其元素值都是元素类型的零值的切片。<br>空切片就是数组指针不为nil，且 <code>slice</code> 的长度为0。<br>nil切片就是引用底层数组指针为 <code>nil</code> 的 <code>slice</code>。</p><p>操作上零切片、空切片和正常的切片都没有任何区别，但是nil切片会多两个特性，一个nil切片等于 <code>nil</code> 值，且进行 <code>json</code> 序列化时其值为 <code>null</code>，nil切片还可以通过赋值为 <code>nil</code> 获得。</p><h2 id="数组与-slice-大比拼"><a href="#数组与-slice-大比拼" class="headerlink" title="数组与 slice 大比拼"></a>数组与 slice 大比拼</h2><p>对数组和 <code>slice</code> 做了性能测试，源码在 <a href="https://github.com/thinkeridea/example/blob/master/array_slice/test/branch_test.go">GitHub</a>。</p><p>对不同容量和数组和切片做性能测试，代码如下，分为：100、1000、10000、100000、1000000、10000000</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkSlice100</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">s := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">100</span>)</span><br><span class="line"><span class="keyword">for</span> i, v := <span class="keyword">range</span> s &#123;</span><br><span class="line">s[i] = <span class="number">1</span> + i</span><br><span class="line">_ = v</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkArray100</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">a := [<span class="number">100</span>]<span class="keyword">int</span>&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> i, v := <span class="keyword">range</span> a &#123;</span><br><span class="line">a[i] = <span class="number">1</span> + i</span><br><span class="line">_ = v</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试结果如下：</p><blockquote><p>goos: darwin<br> goarch: amd64<br> pkg: github.com/thinkeridea/example/array_slice/test<br> BenchmarkSlice100-8             20000000            69.8 ns/op           0 B/op           0 allocs/op<br> BenchmarkArray100-8             20000000            69.0 ns/op           0 B/op           0 allocs/op<br> BenchmarkSlice1000-8             5000000           318 ns/op           0 B/op           0 allocs/op<br> BenchmarkArray1000-8             5000000           316 ns/op           0 B/op           0 allocs/op<br> BenchmarkSlice10000-8             200000          9024 ns/op       81920 B/op           1 allocs/op<br> BenchmarkArray10000-8             500000          3143 ns/op           0 B/op           0 allocs/op<br> BenchmarkSlice100000-8             10000        114398 ns/op      802816 B/op           1 allocs/op<br> BenchmarkArray100000-8             20000         61856 ns/op           0 B/op           0 allocs/op<br> BenchmarkSlice1000000-8             2000        927946 ns/op     8003584 B/op           1 allocs/op<br> BenchmarkArray1000000-8             5000        342442 ns/op           0 B/op           0 allocs/op<br> BenchmarkSlice10000000-8             100      10555770 ns/op    80003072 B/op           1 allocs/op<br> BenchmarkArray10000000-8              50      22918998 ns/op    80003072 B/op           1 allocs/op<br> PASS<br>ok      github.com/thinkeridea/example/array_slice/test    23.333s</p></blockquote><p>从上面的结果可以发现数组和 <code>slice</code> 在1000以内的容量上时性能机会一致，而且都没有内存分配，这应该是编译器对 <code>slice</code> 的特殊优化。<br>从10000~1000000容量时数组的效率就比<code>slice</code>好了一倍有余，主要原因是数组在没有内存分配做了编译优化，而 <code>slice</code> 有内存分配。<br>但是10000000容量往后数组性能大幅度下降，<code>slice</code> 是数组性能的两倍，两个都在运行时做了内存分配，其实这么大的数组还真是不常见，也没有比较做编译器优化了。</p><h2 id="slice-与数组的应用场景总结"><a href="#slice-与数组的应用场景总结" class="headerlink" title="slice 与数组的应用场景总结"></a>slice 与数组的应用场景总结</h2><p><code>slice</code> 和数组有些差别，特别是应用层上，特性差别很大，那什么时间使用数组，什么时间使用切片呢。<br>之前做了性能测试，在1000以内性能几乎一致，只有10000~1000000时才会出现数组性能好于 <code>slice</code>，由于数组在编译时确定长度，也就是再编写程序时必须确认长度，所有往常不会用到更大的数组，大多数都在1000以内的长度。我认为如果在编写程序是就已经确定数据长度，建议用数组，而且竟可能是局部使用的位置建议用数组（避免传递产生值拷贝），比如一天24小时，一小时60分钟，ip是4个 <code>byte</code>这种情况是可以用时数组的。</p><p>为什么推荐用数组，只要能在编写程序是确定数据长度我都会用数组，因为其类型会帮助阅读理解程序，<code>dayHour := [24]Data</code> 一眼就知道是按小时切分数据存储的，如要传递数组时可以考虑传递数组的指针，当然会带来一些操作不方便，往常我使用数组都是不需要传递给其它函数的，可能会在 <code>struct</code> 里面保存数组，然后传递 <code>struct</code> 的指针，或者用 <code>unsafe</code> 来反解析数组指针到新的数组，也不会产生数据拷贝，并且只增加一句转换语句。<code>slice</code> 会比数组多存储三个 <code>int</code> 的属性，而且指针引用会增加 <code>GC</code> 扫描的成本，每次传递都会对这三个属性进行拷贝，如果可以也可以考虑传递 <code>slice</code> 的指针，指针只有一个 <code>int</code> 的大小。</p><p>** 对于不确定大小的数据只能用 <code>slice</code>，否则就要自己做扩容很麻烦， 对于确定大小的集合建议使用数组。**</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;code&gt;array&lt;/code&gt; 和 &lt;code&gt;slice&lt;/code&gt; 看似相似，却有着极大的不同，但他们之间还有着千次万缕的联系 &lt;code&gt;slice&lt;/code&gt; 是引用类型、是 &lt;code&gt;array&lt;/code&gt; 的引用，相当于动态数组，&lt;br&gt;这些都是 &lt;code&gt;slice&lt;/code&gt; 的特性，但是 &lt;code&gt;slice&lt;/code&gt; 底层如何表现，内存中是如何分配的，特别是在程序中大量使用 &lt;code&gt;slice&lt;/code&gt; 的情况下，怎样可以高效使用 &lt;code&gt;slice&lt;/code&gt;？&lt;br&gt;今天借助 &lt;code&gt;Go&lt;/code&gt; 的 &lt;code&gt;unsafe&lt;/code&gt; 包来探索 &lt;code&gt;array&lt;/code&gt; 和 &lt;code&gt;slice&lt;/code&gt; 的各种奥妙。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="unsafe" scheme="https://blog.thinkeridea.com/tags/unsafe/"/>
    
    <category term="slice" scheme="https://blog.thinkeridea.com/tags/slice/"/>
    
    <category term="array" scheme="https://blog.thinkeridea.com/tags/array/"/>
    
  </entry>
  
  <entry>
    <title>【go】一次读锁重入导致的死锁故障</title>
    <link href="https://blog.thinkeridea.com/201812/go/yi_ci_du_suo_chong_ru_dao_zhi_de_si_suo_gu_zhang.html"/>
    <id>https://blog.thinkeridea.com/201812/go/yi_ci_du_suo_chong_ru_dao_zhi_de_si_suo_gu_zhang.html</id>
    <published>2018-12-25T14:30:10.000Z</published>
    <updated>2021-01-04T01:59:33.091Z</updated>
    
    <content type="html"><![CDATA[<p>在两天前第一次遇到自己的程序出现死锁， 我一直非常的小心使用锁，了解死锁导致的各种可能性，<br>这次的经历让我未来会更加小心，下面来回顾一下死锁发生的过程与代码演进的过程吧。</p><h2 id="简述业务背景及代码演进过程"><a href="#简述业务背景及代码演进过程" class="headerlink" title="简述业务背景及代码演进过程"></a>简述业务背景及代码演进过程</h2><p>我的程序中有一块缓存，数据会组织好放到内存中，会根据数据源（MySQL）更新而刷新缓存，是读多写少的应用场景。<br>内存中有一个很大数据列表，缓存模块会按数据维度进行分组，每次访问根据维度查找到这个列表里面的所有数据。<br>业务模块拿到数据后会根据业务需要再做一次筛选，选出N个符合条件的数据（具体多少个由业务模块的规则决定）。</p><a id="more"></a><p>以下是简化的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cache</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Cache <span class="keyword">struct</span> &#123;</span><br><span class="line">lock sync.RWMutex</span><br><span class="line">data []<span class="keyword">int</span> <span class="comment">// 实际数据比这个复杂很多有很多维度</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span> <span class="title">Get</span><span class="params">()</span> []<span class="title">int</span></span> &#123;</span><br><span class="line">c.lock.RLock()</span><br><span class="line"><span class="keyword">defer</span> c.lock.RUnlock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> res []<span class="keyword">int</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 筛选数据， 简单写一个筛选过程</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> c.data &#123;</span><br><span class="line"><span class="keyword">if</span> c.data[i] &gt; <span class="number">10</span> &#123;</span><br><span class="line">res = <span class="built_in">append</span>(res, c.data[i])</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> res</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个方法返回的数据会很多，可实际业务需要的数据只有几个而已，那做一个优化吧，利用 <code>go</code> 的 <code>chan</code> 实现一个迭代生成器，每次只返回一个数据，业务端找到需要的数据后立即终止。</p><p>调整后的方法大致像下面这样:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cache</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Cache <span class="keyword">struct</span> &#123;</span><br><span class="line">lock sync.RWMutex</span><br><span class="line">data []<span class="keyword">int</span> <span class="comment">// 实际数据比这个复杂很多有很多维度</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span> <span class="title">Get</span><span class="params">(next <span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span> <span class="title">chan</span> <span class="title">int</span></span> &#123;</span><br><span class="line">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">c.lock.RLock()</span><br><span class="line"><span class="keyword">defer</span> c.lock.RUnlock()</span><br><span class="line"><span class="keyword">defer</span> <span class="built_in">close</span>(ch)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 筛选数据， 简单写一个筛选过程</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> c.data &#123;</span><br><span class="line"><span class="keyword">if</span> c.data[i] &gt; <span class="number">10</span> &#123;</span><br><span class="line"></span><br><span class="line">ch &lt;- i</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, ok := &lt;-next; !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ch</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用端的代码类似下面这样：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">data := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">10</span>)</span><br><span class="line">c := Cache&#123;&#125;</span><br><span class="line">next := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> c.Get(next) &#123;</span><br><span class="line">    data = <span class="built_in">append</span>(data, i)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(data) &gt;= <span class="number">10</span> &#123;</span><br><span class="line">        <span class="built_in">close</span>(next)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    next &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样调整后查看程序的内存分配显著降低，而且平安无事在生产环境运行了半个月^_^，当然截止当前还不会出现死锁的情况。<br>有一天业务调整了，在 <code>cache</code> 模块有另外一个方法，公用这个锁（实际我缓存模块为了统一，都使用一个锁，方便管理），下面的代码也写到这个 <code>cache</code> 组件里面。</p><p>以下代码只增加了改变的部分，<code>....</code> 保持原来的代码不变。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cache</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Cache <span class="keyword">struct</span> &#123;</span><br><span class="line">....</span><br><span class="line">x <span class="keyword">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span> <span class="title">XX</span><span class="params">(i <span class="keyword">int</span>)</span> <span class="title">int</span></span>&#123;</span><br><span class="line">c.lock.RLock()</span><br><span class="line"><span class="keyword">defer</span> c.lock.RUnlock()</span><br><span class="line">    </span><br><span class="line"><span class="keyword">if</span>  i &gt;c.x &#123;</span><br><span class="line"><span class="keyword">return</span> i</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line">....</span><br></pre></td></tr></table></figure><p>添加一个方法怎么就导致死锁了呢，主要是调用端的业务代码也发生变化了，更改如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">data := <span class="built_in">make</span>([]<span class="keyword">int</span>, <span class="number">0</span>, <span class="number">10</span>)</span><br><span class="line">c := Cache&#123;&#125;</span><br><span class="line">next := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> c.Get(next) &#123;</span><br><span class="line">    data = <span class="built_in">append</span>(data, i)</span><br><span class="line">    <span class="keyword">if</span> c.XX(i) != i  &#123; <span class="comment">// 在这里调用了缓存模块的另一个方法</span></span><br><span class="line">        <span class="built_in">close</span>(next)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    next &lt;- <span class="keyword">struct</span>&#123;&#125;&#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修改后的代码上线存活了5天就挂了，实际是当时业务订单需求很少，只是有很多流量请求，并没有频繁访问这个方法，否者会在极短的时间导致死锁，<br>通过这块简化的代码，也很难分析出会导致死锁，真实的业务代码很多，而且调用关系比较复杂，我们通过代码审核并没有发现任何问题。</p><h2 id="事故现场分析排查问题"><a href="#事故现场分析排查问题" class="headerlink" title="事故现场分析排查问题"></a>事故现场分析排查问题</h2><p>上线5天后突然接到服务无法响应的报警，事故发生立即查看了 <code>grafana</code> 的监控数据，发现在极段时间内服务器资源消耗极速增长，然后就立即没有响应了</p><p><img src="/assets/image/20181225/20181219-011353.jpg"></p><p>通过业务监控发现服务在极端的时间打开近10万个 <code>goroutine</code> 之后持续了很长一段时间，<br><code>cpu</code> 占用和 <code>gc</code> 都很正常， 内存方面可以看出短时间内分配了很多内存，但是没有被释放，<code>gc</code> 没法回收说明一直被占用，</p><p>看到这里我心里在想可能是有个 <code>goroutine</code> 因为什么原因导致无法结束造成的事故吧，<br>然后我再往下看（实际页面是在需要滚动屏幕，第一屏只显示了上面6个模块），发现 open files 和 <code>goroutine</code> 的情况一致，并且之后的数据突然中断，<br>中断是因为服务无法影响，也就无法采集服务的信息了。</p><p><img src="/assets/image/20181225/openfd.jpg"></p><p><code>goroutine</code> 并不会占用 open files，一个http服务导致这种情况大概只能是网络连接过多，我们遭受攻击了吗……<br>显然是没有的不然cpu不能很正常，那就是有可能请求无法响应，什么原因导致呢？</p><p>使用 <code>lsof -n | grep dsp | wc -l</code> 命令去服务器查找服务打开文件数，确实在六万五千多，<br>通过 <code>cat /proc/30717/limits</code> 发现 <code>Max open files            65535                65535                files</code>，<br>配置的最大打开文件数只有 65535，使用 <code>lsof -n | grep dsp |grep TCP | wc -l</code> 发现数据和之前接近，只小了几个，那是日志文件占用的。</p><p>查看日志发现大量 <code>http: Accept error: accept tcp 172.17.191.231:8090: accept4: too many open files; retrying in 1s</code> 错误。</p><p>这些数据帮助我快速定位确实是有请求发送到服务器，服务器无法响应导致短时间内占用很多文件打开数，导致系统限制无法建立新的连接。<br>这里要说一下，即使客户端断开连接了，服务器连接还是没有办法关闭，因为 <code>goroutine</code> 没有办法关闭， 除非自己退出。</p><p>找到原因了，服务没法响应，没法通过现场查找问题了，先重新启动一下服务，恢复业务在查找代码问题。</p><p>接下来就是查找代码问题了，期间又出现了一次故障，立即重启服务，恢复业务。</p><h2 id="分析解决问题"><a href="#分析解决问题" class="headerlink" title="分析解决问题"></a>分析解决问题</h2><p>通过几个小时分析代码逻辑，终于有了进展，发现上面的示例代码逻辑块导致读锁重入，存在死锁风险，这种死锁的碰撞概率非常低，<br>之前说过我们的缓存是读多写少的场景，如果只是读取数据，上面的代码不会有任何问题，我们一天刷新缓存的次数也不过百余次而已。</p><p>看一下究竟发生了什么导致的死锁吧：</p><ul><li>程序执行 <code>cache.Get</code> 获取一个 <code>chan</code>, 在 <code>cache.Get</code> 里面有一个 <code>goroutine</code> 读取数据只有加了读写锁，只有 <code>goroutine</code> 关闭才会释放</li><li><code>for i := range c.Get(next) &#123;</code> 遍历 <code>chan</code> 时 <code>goroutine</code> 不会结束，也就说读锁没有被释放</li><li>遍历时执行了 <code>c.XX(i)</code> 方法，在该方面里面也加了读锁， 形成了读锁重入的场景，但是该放执行周期很短，执行完就会马上释放</li></ul><p>好吧，这样的流程并没有形成死锁，什么情况下导致的死锁呢，接着看一下一个场景：</p><ul><li>程序执行 <code>cache.Get</code> 获取一个 <code>chan</code>, 在 <code>cache.Get</code> 里面有一个 <code>goroutine</code> 读取数据只有加了读写锁，只有 <code>goroutine</code> 关闭才会释放</li><li><code>for i := range c.Get(next) &#123;</code> 遍历 <code>chan</code> 时 <code>goroutine</code> 不会结束，也就说读锁没有被释放</li><li>数据发生了改变，触发了缓存刷新，申请独占锁（写锁），等待所有读锁释放</li><li>遍历时执行 <code>c.XX(i)</code> 方法，该方法申请读锁，因为写锁在等待，所以任何读锁都将等待写锁释放后才能添加成功</li><li>for 循环被阻塞， <code>cache.Get</code> 里面的 <code>goroutine</code> 无法退出，无法释放读锁</li><li>写锁等待所有读锁释放</li><li><code>c.XX(i)</code> 等待写锁释放</li><li>….</li></ul><p>重点看第三步，这里是关键，因为在两个嵌套的读锁中间申请写锁，导致死锁发生，找到原因修复起来很简单的，</p><p>调整 <code>cache.Get</code> 加锁的方法，把 <code>c.data</code> 赋值给一个临时变量 <code>data</code>, 在这段代码前后加锁和释放锁，锁的代码块更小，时间更短</p><p><code>c.data</code> 单独拷贝是安全的，那怕是指针数据，因为每次刷新缓存都会给 <code>c.data</code> 重新赋值，分配新的内存空间。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cache</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Cache <span class="keyword">struct</span> &#123;</span><br><span class="line">lock sync.RWMutex</span><br><span class="line">data []<span class="keyword">int</span> <span class="comment">// 实际数据比这个复杂很多有很多维度</span></span><br><span class="line">x <span class="keyword">int</span></span><br><span class="line">&#125;</span><br><span class="line">    </span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span> <span class="title">XX</span><span class="params">(i <span class="keyword">int</span>)</span> <span class="title">int</span></span>&#123;</span><br><span class="line">    c.lock.RLock()</span><br><span class="line">    <span class="keyword">defer</span> c.lock.RUnlock()</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span>  i &gt;c.x &#123;</span><br><span class="line">        <span class="keyword">return</span> i</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Cache)</span> <span class="title">Get</span><span class="params">(next <span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span> <span class="title">chan</span> <span class="title">int</span></span> &#123;</span><br><span class="line">ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">defer</span> <span class="built_in">close</span>(ch)</span><br><span class="line"></span><br><span class="line">c.lock.RLock()</span><br><span class="line">data := c.data</span><br><span class="line">c.lock.RUnlock()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 筛选数据， 简单写一个筛选过程</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="keyword">range</span> data &#123;</span><br><span class="line"><span class="keyword">if</span> data[i] &gt; <span class="number">10</span> &#123;</span><br><span class="line"></span><br><span class="line">ch &lt;- i</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, ok := &lt;-next; !ok &#123;</span><br><span class="line"><span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> ch</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修复之后的业务状态：</p><p><img src="/assets/image/20181225/20181219-011418.jpg"></p><h2 id="复现问题"><a href="#复现问题" class="headerlink" title="复现问题"></a>复现问题</h2><p>用程序复现一下上面的场景可以吗，好像有点难，我写了一个简单的复现代码，如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;runtime&quot;</span></span><br><span class="line"><span class="string">&quot;sync&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> l = sync.RWMutex&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">wg.Add(<span class="number">2</span>)</span><br><span class="line">c := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>)</span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">l.RLock() <span class="comment">// 读锁1</span></span><br><span class="line"><span class="keyword">defer</span> l.RUnlock()</span><br><span class="line">fmt.Println(<span class="number">1</span>)</span><br><span class="line">c &lt;- <span class="number">1</span></span><br><span class="line">fmt.Println(<span class="number">2</span>)</span><br><span class="line">runtime.Gosched()</span><br><span class="line">fmt.Println(<span class="number">3</span>)</span><br><span class="line">b()</span><br><span class="line">fmt.Println(<span class="number">4</span>)</span><br><span class="line">wg.Done()</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Println(<span class="number">5</span>)</span><br><span class="line">&lt;-c</span><br><span class="line">fmt.Println(<span class="number">6</span>)</span><br><span class="line">l.Lock()</span><br><span class="line">fmt.Println(<span class="number">7</span>)</span><br><span class="line">fmt.Println(<span class="number">8</span>)</span><br><span class="line"><span class="keyword">defer</span> l.Unlock()</span><br><span class="line">fmt.Println(<span class="number">9</span>)</span><br><span class="line">wg.Done()</span><br><span class="line">&#125;()</span><br><span class="line"></span><br><span class="line"><span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">i := <span class="number">1</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">i++</span><br><span class="line">&#125;</span><br><span class="line">&#125;()</span><br><span class="line">wg.Wait()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">b</span><span class="params">()</span></span> &#123;</span><br><span class="line">fmt.Println(<span class="number">10</span>)</span><br><span class="line">l.RLock() <span class="comment">// 读锁2</span></span><br><span class="line">fmt.Println(<span class="number">11</span>)</span><br><span class="line"><span class="keyword">defer</span> l.RUnlock()</span><br><span class="line">fmt.Println(<span class="number">12</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段程序的输出(受 <code>goroutine</code> 运行时影响在输出数字3之前会有些许差异)：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">10</span><br></pre></td></tr></table></figure><p>分析一下这个运行流程吧:</p><ul><li>首先加上读锁1，就是 <code>fmt.Println(1)</code> 之前， 状态加读锁1</li><li>另外一个 <code>goroutine</code> 启动，<code>fmt.Println(5)</code>， 状态加读锁1</li><li>发送数据 <code>c &lt;- 1</code> ， 状态加读锁1</li><li>接受到数据 <code>&lt;-c</code> <code>fmt.Println(6)</code>， 状态加读锁1</li><li>输出 2 <code>fmt.Println(2)</code>， 状态加读锁1</li><li>暂停当前 <code>goroutine</code> <code>runtime.Gosched()</code> ， 状态加读锁1</li><li>申请写锁 <code>l.Lock()</code>， 等待读锁1释放， 状态加读锁1、写锁等待</li><li>切换 <code>goroutine</code> 执行 <code>fmt.Println(3)</code> 与 <code>b()</code>， 状态加读锁1、写锁等待</li><li>输出10 <code>fmt.Println(10)</code>， 申请读锁2，等待写锁释放， 状态加读锁1、写锁等待、读锁2等待</li><li>支持程序永久阻塞……</li></ul><h2 id="分析读写锁实现"><a href="#分析读写锁实现" class="headerlink" title="分析读写锁实现"></a>分析读写锁实现</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">RLock</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">_ = rw.w.state</span><br><span class="line">race.Disable()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> atomic.AddInt32(&amp;rw.readerCount, <span class="number">1</span>) &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="comment">// A writer is pending, wait for it.</span></span><br><span class="line">runtime_SemacquireMutex(&amp;rw.readerSem, <span class="literal">false</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">race.Acquire(unsafe.Pointer(&amp;rw.readerSem))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>申请写锁时会在 <code>rw.readerCount</code> 读数量变量上自增加 1，如果结果小于 0，当前读锁进入修改等待读锁唤醒信号，<br>单独看着一个方法会比较懵，为啥读的数量会小于0呢，接着看写锁。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">Lock</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">_ = rw.w.state</span><br><span class="line">race.Disable()</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// First, resolve competition with other writers.</span></span><br><span class="line">rw.w.Lock()</span><br><span class="line"><span class="comment">// Announce to readers there is a pending writer.</span></span><br><span class="line">r := atomic.AddInt32(&amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders</span><br><span class="line"><span class="comment">// Wait for active readers.</span></span><br><span class="line"><span class="keyword">if</span> r != <span class="number">0</span> &amp;&amp; atomic.AddInt32(&amp;rw.readerWait, r) != <span class="number">0</span> &#123;</span><br><span class="line">runtime_SemacquireMutex(&amp;rw.writerSem, <span class="literal">false</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">race.Acquire(unsafe.Pointer(&amp;rw.readerSem))</span><br><span class="line">race.Acquire(unsafe.Pointer(&amp;rw.writerSem))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>申请写锁时会先加上互斥锁，也就是有其它写的客户端的话会等待写锁释放才能加上，具体实现看互斥锁的代码，<br>然后在 <code>rw.readerCount</code> 上自增一个极大的负数 <code>1 &lt;&lt; 30</code> ， 读写锁这里也就限制了我们的同时读的进程不能超过这个值。<br>然后在结果上加上 <code>rwmutexMaxReaders</code> 也就是 <code>atomic.AddInt32(&amp;rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders</code> 得到实际读客户端的数量<br>如果读的客户端不等于0，就在 <code>rw.readerWait</code> 自增读客户端的数量，之后陷入睡眠，等待 <code>rw.writerSem</code> 唤醒。</p><p>分析了这两段代码我们就能明白，写锁等待或者添加时，读锁没法添加上</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">RUnlock</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">_ = rw.w.state</span><br><span class="line">race.ReleaseMerge(unsafe.Pointer(&amp;rw.writerSem))</span><br><span class="line">race.Disable()</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> r := atomic.AddInt32(&amp;rw.readerCount, <span class="number">-1</span>); r &lt; <span class="number">0</span> &#123;</span><br><span class="line"><span class="keyword">if</span> r+<span class="number">1</span> == <span class="number">0</span> || r+<span class="number">1</span> == -rwmutexMaxReaders &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">throw(<span class="string">&quot;sync: RUnlock of unlocked RWMutex&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// A writer is pending.</span></span><br><span class="line"><span class="keyword">if</span> atomic.AddInt32(&amp;rw.readerWait, <span class="number">-1</span>) == <span class="number">0</span> &#123;</span><br><span class="line"><span class="comment">// The last reader unblocks the writer.</span></span><br><span class="line">runtime_Semrelease(&amp;rw.writerSem, <span class="literal">false</span>)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>释放读锁，先在 <code>rw.readerCount</code> 减 1，然后检查读客户端是否小于0，如果小于0说明有写锁在等待，<br>在 <code>rw.readerWait</code> 上减1，这个变量记录的是写等待读客户端的数量，如果没有需要等待的读客户端了，就通知 <code>rw.writerSem</code> 唤醒写锁</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(rw *RWMutex)</span> <span class="title">Unlock</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">_ = rw.w.state</span><br><span class="line">race.Release(unsafe.Pointer(&amp;rw.readerSem))</span><br><span class="line">race.Disable()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Announce to readers there is no active writer.</span></span><br><span class="line">r := atomic.AddInt32(&amp;rw.readerCount, rwmutexMaxReaders)</span><br><span class="line"><span class="keyword">if</span> r &gt;= rwmutexMaxReaders &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">throw(<span class="string">&quot;sync: Unlock of unlocked RWMutex&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Unblock blocked readers, if any.</span></span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="keyword">int</span>(r); i++ &#123;</span><br><span class="line">runtime_Semrelease(&amp;rw.readerSem, <span class="literal">false</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Allow other writers to proceed.</span></span><br><span class="line">rw.w.Unlock()</span><br><span class="line"><span class="keyword">if</span> race.Enabled &#123;</span><br><span class="line">race.Enable()</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写锁在释放时会给 <code>rw.readerCount</code> 自增 <code>rwmutexMaxReaders</code> 还原真实读客户端数量。<br><code>for i := 0; i &lt; int(r); i++ &#123;</code> 用来唤醒所有的读客户端，因为在写锁的时候，申请读锁的客户端会被计数，但是都会陷入睡眠状态。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以前特别强调过读锁重入导致死锁的问题，而且这个问题非常难在业务代码里面复现，触发几率很低，<br>编译和运行时都无法检测这种情况，所以千万不能陷入读锁重入的嵌套使用的情况，否者问题非常难以排查。</p><p>关于加锁的几个小经验：</p><ul><li>运行时离开当前逻辑就释放锁。</li><li>锁的粒度越小越好，加锁后尽快释放锁。</li><li>尽量不用 <code>defer</code> 释放锁。</li><li>读锁不要嵌套。</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;在两天前第一次遇到自己的程序出现死锁， 我一直非常的小心使用锁，了解死锁导致的各种可能性，&lt;br&gt;这次的经历让我未来会更加小心，下面来回顾一下死锁发生的过程与代码演进的过程吧。&lt;/p&gt;
&lt;h2 id=&quot;简述业务背景及代码演进过程&quot;&gt;&lt;a href=&quot;#简述业务背景及代码演进过程&quot; class=&quot;headerlink&quot; title=&quot;简述业务背景及代码演进过程&quot;&gt;&lt;/a&gt;简述业务背景及代码演进过程&lt;/h2&gt;&lt;p&gt;我的程序中有一块缓存，数据会组织好放到内存中，会根据数据源（MySQL）更新而刷新缓存，是读多写少的应用场景。&lt;br&gt;内存中有一个很大数据列表，缓存模块会按数据维度进行分组，每次访问根据维度查找到这个列表里面的所有数据。&lt;br&gt;业务模块拿到数据后会根据业务需要再做一次筛选，选出N个符合条件的数据（具体多少个由业务模块的规则决定）。&lt;/p&gt;</summary>
    
    
    
    <category term="go" scheme="https://blog.thinkeridea.com/categories/go/"/>
    
    
    <category term="读写锁" scheme="https://blog.thinkeridea.com/tags/%E8%AF%BB%E5%86%99%E9%94%81/"/>
    
    <category term="死锁" scheme="https://blog.thinkeridea.com/tags/%E6%AD%BB%E9%94%81/"/>
    
  </entry>
  
</feed>
