<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Gogeof-分享</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://yoursite.com/"/>
  <updated>2020-05-02T14:07:56.068Z</updated>
  <id>http://yoursite.com/</id>
  
  <author>
    <name>Gogeof</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>【译】Golang 中的标签</title>
    <link href="http://yoursite.com/2019/01/27/%E3%80%90%E8%AF%91%E3%80%91Golang-%E4%B8%AD%E7%9A%84%E6%A0%87%E7%AD%BE/"/>
    <id>http://yoursite.com/2019/01/27/%E3%80%90%E8%AF%91%E3%80%91Golang-%E4%B8%AD%E7%9A%84%E6%A0%87%E7%AD%BE/</id>
    <published>2019-01-27T14:06:54.000Z</published>
    <updated>2020-05-02T14:07:56.068Z</updated>
    
    <content type="html"><![CDATA[<p>结构体字段的声明可以通过之后放置的文字来标记。标签添加由当前包或外部包使用的元信息。让我们首先回首一下 strcut 声明的样子，然后我们将扔出几个用例，深入研究这个标签。</p><a id="more"></a><h2 id="结构体类型（Struct-type）"><a href="#结构体类型（Struct-type）" class="headerlink" title="结构体类型（Struct type）"></a>结构体类型（Struct type）</h2><p>Struct 是一系列字段。每个字段由可选名称和所需类型（<a href="https://play.golang.org/p/q2V_op8_SJk" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="keyword">type</span> T1 <span class="keyword">struct</span> &#123;</span><br><span class="line">    f1 <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> T2 <span class="keyword">struct</span> &#123;</span><br><span class="line">    T1</span><br><span class="line">    f2     <span class="keyword">int64</span></span><br><span class="line">    f3, f4 <span class="keyword">float64</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := T2&#123;T1&#123;<span class="string">"foo"</span>&#125;, <span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">    fmt.Println(t.f1)    <span class="comment">// foo</span></span><br><span class="line">    fmt.Println(t.T1.f1) <span class="comment">// foo</span></span><br><span class="line">    fmt.Println(t.f2)    <span class="comment">// 1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>T1 域被称为嵌入字段，因为它是用类型声明但没有名称。</p><p>字段声明可以在 T2 中指定来自第 3 个字段声明的 f3 和 f4 之类的多个标识符。</p><p>语言规范声明每个字段声明后面跟着分号，但正如我们上面所见，它可以省略。如果需要将多个字段声明放入同一行（源代码），分号可能很有用（<a href="https://play.golang.org/p/nTTgVX7BqV8" target="_blank" rel="noopener">源代码</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="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f1 <span class="keyword">int64</span>; f2 <span class="keyword">float64</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := T&#123;<span class="number">1</span>, <span class="number">2</span>&#125;</span><br><span class="line">    fmt.Println(t.f1, t.f2)  <span class="comment">// 1 2</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="标签（Tag）"><a href="#标签（Tag）" class="headerlink" title="标签（Tag）"></a>标签（Tag）</h2><p>字段声明后面可以跟一个可选的字符串文字（标记），它称为相应字段声明中所有字段的属性（单字段声明可以指定多个标识符）。让我们看看它的实际应用（<a href="https://play.golang.org/p/BubxnOxpOcM" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f1     <span class="keyword">string</span> <span class="string">"f one"</span></span><br><span class="line">    f2     <span class="keyword">string</span></span><br><span class="line">    f3     <span class="keyword">string</span> <span class="string">`f three`</span></span><br><span class="line">    f4, f5 <span class="keyword">int64</span>  <span class="string">`f four and five`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以使用原始字符串文字或解释的字符串文字，但下面描述的传统格式需要原始字符串文字。<a href="https://golang.org/ref/spec#String_literals" target="_blank" rel="noopener">规范</a>中描述了原始字符串文字和解释字符串文字之间的差异。</p><p>如果字段声明包含多个标识符，则标记将附加到字段声明的所有字段（如上面的字段 f4 和 f5）。</p><h2 id="反射（Reflection）"><a href="#反射（Reflection）" class="headerlink" title="反射（Reflection）"></a>反射（Reflection）</h2><p>标签可通过 reflect 包访问，允许运行时反射（<a href="https://play.golang.org/p/YYEAfGc6iaE" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">    <span class="string">"reflect"</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f1     <span class="keyword">string</span> <span class="string">"f one"</span></span><br><span class="line">    f2     <span class="keyword">string</span></span><br><span class="line">    f3     <span class="keyword">string</span> <span class="string">`f three`</span></span><br><span class="line">    f4, f5 <span class="keyword">int64</span>  <span class="string">`f four and five`</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(T&#123;&#125;)</span><br><span class="line">    f1, _ := t.FieldByName(<span class="string">"f1"</span>)</span><br><span class="line">    fmt.Println(f1.Tag) <span class="comment">// f one</span></span><br><span class="line">    f4, _ := t.FieldByName(<span class="string">"f4"</span>)</span><br><span class="line">    fmt.Println(f4.Tag) <span class="comment">// f four and five</span></span><br><span class="line">    f5, _ := t.FieldByName(<span class="string">"f5"</span>)</span><br><span class="line">    fmt.Println(f5.Tag) <span class="comment">// f four and five</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>设置空标记与完全不使用标记的效果相同（<a href="https://play.golang.org/p/u5VUMXz01cJ" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f1 <span class="keyword">string</span> <span class="string">``</span></span><br><span class="line">    f2 <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(T&#123;&#125;)</span><br><span class="line">    f1, _ := t.FieldByName(<span class="string">"f1"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%q\n"</span>, f1.Tag) <span class="comment">// ""</span></span><br><span class="line">    f2, _ := t.FieldByName(<span class="string">"f2"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%q\n"</span>, f2.Tag) <span class="comment">// ""</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="常规格式（Conventional-format）"><a href="#常规格式（Conventional-format）" class="headerlink" title="常规格式（Conventional format）"></a>常规格式（Conventional format）</h2><p>在提交中引入“反射：支持多个包使用结构标记”允许为每个包设置元信息。这提供了简单的命名空间。标签被格式化为键的串联：“值”对。密钥可能是像 json 这样的包的名称。对可以选择用空格分隔 - key1:“ value1” key2: “value2” key3: “value3”。 如果使用传统格式，那么我们可以使用两种 struct tag （StructTag）方法 - Get 或 Lookup。它们允许返回与所需键内部标记相关联的值。</p><p>Lookup 函数返回两个值 - 与键关联的值（如果未设置则为空）和 bool，指示是否已找到键（<a href="https://play.golang.org/p/i2Vh3tfN3A5" target="_blank" rel="noopener">源代码</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="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f <span class="keyword">string</span> <span class="string">`one:"1" two:"2"blank:""`</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(T&#123;&#125;)</span><br><span class="line">    f, _ := t.FieldByName(<span class="string">"f"</span>)</span><br><span class="line">    fmt.Println(f.Tag) <span class="comment">// one:"1" two:"2"blank:""</span></span><br><span class="line">    v, ok := f.Tag.Lookup(<span class="string">"one"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%s, %t\n"</span>, v, ok) <span class="comment">// 1, true</span></span><br><span class="line">    v, ok = f.Tag.Lookup(<span class="string">"blank"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%s, %t\n"</span>, v, ok) <span class="comment">// , true</span></span><br><span class="line">    v, ok = f.Tag.Lookup(<span class="string">"five"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%s, %t\n"</span>, v, ok) <span class="comment">// , false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Get 方法只是 Lookup 简单的包封装器，它丢弃了 bool 值（<a href="https://github.com/golang/go/blob/1b1c8b34d129eefcdbad234914df999581e62b2f/src/reflect/type.go#L1163" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(tag StructTag)</span> <span class="title">Get</span><span class="params">(key <span class="keyword">string</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    v, _ := tag.Lookup(key)</span><br><span class="line">    <span class="keyword">return</span> v</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果标签不是常规模式，则不指定 Get 或 Lookup 的返回值。</p><p>即使 tag 是任何字符串值（不管是释义或原始值），只有在双引号（<a href="">源代码</a>）之间包含值时，Lookup 和 Get 方法才会找到 key 的值：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f <span class="keyword">string</span> <span class="string">"one:`1`"</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(T&#123;&#125;)</span><br><span class="line">    f, _ := t.FieldByName(<span class="string">"f"</span>)</span><br><span class="line">    fmt.Println(f.Tag) <span class="comment">// one:`1`</span></span><br><span class="line">    v, ok := f.Tag.Lookup(<span class="string">"one"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%s, %t\n"</span>, v, ok) <span class="comment">// , false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以在解释的字符串值中使用转义双引号（<a href="https://play.golang.org/p/o5APz18OH6e" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f <span class="keyword">string</span> <span class="string">"one:\"1\""</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    t := reflect.TypeOf(T&#123;&#125;)</span><br><span class="line">    f, _ := t.FieldByName(<span class="string">"f"</span>)</span><br><span class="line">    fmt.Println(f.Tag) <span class="comment">// one:"1"</span></span><br><span class="line">    v, ok := f.Tag.Lookup(<span class="string">"one"</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"%s, %t\n"</span>, v, ok) <span class="comment">// 1, true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但可读性就要低很多。</p><h2 id="结论（Conversion）"><a href="#结论（Conversion）" class="headerlink" title="结论（Conversion）"></a>结论（Conversion）</h2><p>将结构体类型转换为其他类型要求底层类型相同，但忽略掉 tag（<a href="https://play.golang.org/p/C7M0bVwYFK2" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T1 <span class="keyword">struct</span> &#123;</span><br><span class="line">     f <span class="keyword">int</span> <span class="string">`json:"foo"`</span></span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">type</span> T2 <span class="keyword">struct</span> &#123;</span><br><span class="line">     f <span class="keyword">int</span> <span class="string">`json:"bar"`</span></span><br><span class="line"> &#125;</span><br><span class="line"> t1 := T1&#123;<span class="number">10</span>&#125;</span><br><span class="line"> <span class="keyword">var</span> t2 T2</span><br><span class="line"> t2 = T2(t1)</span><br><span class="line"> fmt.Println(t2) <span class="comment">// &#123;10&#125;</span></span><br></pre></td></tr></table></figure><p>Go 1.8 （提案）中引入了此行为。在 Go 1.7 及更早版本的代码中，可能会抛出编译时错误。</p><h2 id="用例（Use-cases）"><a href="#用例（Use-cases）" class="headerlink" title="用例（Use cases）"></a>用例（Use cases）</h2><h3 id="Un-marshaling"><a href="#Un-marshaling" class="headerlink" title="(Un)marshaling"></a>(Un)marshaling</h3><p>Go 中标签最常见的用途可能是编组。让我们看一下来自 json 包的函数 Marshal 如何使用它（<a href="https://play.golang.org/p/C1hAMXTKPM_S" target="_blank" rel="noopener">源代码</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"encoding/json"</span></span><br><span class="line">    <span class="string">"fmt"</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">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">       F1 <span class="keyword">int</span> <span class="string">`json:"f_1"`</span></span><br><span class="line">       F2 <span class="keyword">int</span> <span class="string">`json:"f_2,omitempty"`</span></span><br><span class="line">       F3 <span class="keyword">int</span> <span class="string">`json:"f_3,omitempty"`</span></span><br><span class="line">       F4 <span class="keyword">int</span> <span class="string">`json:"-"`</span></span><br><span class="line">    &#125;</span><br><span class="line">    t := T&#123;<span class="number">1</span>, <span class="number">0</span>, <span class="number">2</span>, <span class="number">3</span>&#125;</span><br><span class="line">    b, err := json.Marshal(t)</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">    fmt.Printf(<span class="string">"%s\n"</span>, b) <span class="comment">// &#123;"f_1":1,"f_3":2&#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>xml 包也利用了标签 - <a href="https://golang.org/pkg/encoding/xml/#MarshalIndent" target="_blank" rel="noopener">https://golang.org/pkg/encoding/xml/#MarshalIndent</a>.</p><h2 id="ORM"><a href="#ORM" class="headerlink" title="ORM"></a>ORM</h2><p>像 GORM 这样的对象关系映射工具，也广泛使用标签 - <a href="https://github.com/jinzhu/gorm/blob/58e34726dfc069b558038efbaa25555f182d1f7a/multi_primary_keys_test.go#L10" target="_blank" rel="noopener">例子</a>.</p><h2 id="摘要数据（Digesting-forms-data）"><a href="#摘要数据（Digesting-forms-data）" class="headerlink" title="摘要数据（Digesting forms data）"></a>摘要数据（Digesting forms data）</h2><p><a href="https://godoc.org/github.com/gorilla/schema" target="_blank" rel="noopener">https://godoc.org/github.com/gorilla/schema</a></p><h2 id="其他（Other）"><a href="#其他（Other）" class="headerlink" title="其他（Other）"></a>其他（Other）</h2><p>标签的更多潜在用例，如配置管理，结构的默认值，验证，命令行参数描述等（<a href="https://github.com/golang/go/wiki/Well-known-struct-tags" target="_blank" rel="noopener">众所周知的结构标记列表</a>）。</p><h2 id="go-vet"><a href="#go-vet" class="headerlink" title="go vet"></a>go vet</h2><p>Go 编译器没有强制执行传统的 struct 标签格式，但是 vet 就是这样做的，所以值得使用它，例如作为 CI 管道的一部分。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    f <span class="keyword">string</span> <span class="string">"one two three"</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;&#125;</span><br><span class="line">&gt; <span class="keyword">go</span> vet tags.<span class="keyword">go</span></span><br><span class="line">tags.<span class="keyword">go</span>:<span class="number">4</span>: <span class="keyword">struct</span> field tag <span class="string">`one two three`</span> not compatible with reflect.StructTag.Get: bad syntax <span class="keyword">for</span> <span class="keyword">struct</span> tag pair</span><br></pre></td></tr></table></figure><p>…</p><p>由于 struct 标签，程序员可以从单一来源中受益。Go 是一种使用语言，所以即使可以使用专用数据结构等其他方式来控制整个过程来解决 JSON/XML 编码，Golang 也能让软件工程师的生活变得更轻松。值得一提的是，标签的长度不受规格的限制。</p><hr><p>via: <a href="https://medium.com/golangspec/tags-in-golang-3e5db0b8ef3e" target="_blank" rel="noopener">https://medium.com/golangspec/tags-in-golang-3e5db0b8ef3e</a></p><p>作者：<a href="https://medium.com/@mlowicki" target="_blank" rel="noopener">Michał Łowicki</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/校对者ID" target="_blank" rel="noopener">校对者ID</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;结构体字段的声明可以通过之后放置的文字来标记。标签添加由当前包或外部包使用的元信息。让我们首先回首一下 strcut 声明的样子，然后我们将扔出几个用例，深入研究这个标签。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go GC</title>
    <link href="http://yoursite.com/2018/11/05/%E3%80%90%E8%AF%91%E3%80%91Go-GC/"/>
    <id>http://yoursite.com/2018/11/05/%E3%80%90%E8%AF%91%E3%80%91Go-GC/</id>
    <published>2018-11-05T14:05:28.000Z</published>
    <updated>2020-05-02T14:12:20.776Z</updated>
    
    <content type="html"><![CDATA[<p>大家好，我是 Okada（<a href="https://twitter.com/ocadaruma" target="_blank" rel="noopener">@ocadaruma</a>）,LINE 广告平台团队的成员。我对 Go 的 GC （垃圾收集器）有点感兴趣，甚至还促使我专门写一篇关于它的博客。Go 是一门由 Google 开发，并且支持垃圾收集的编程语言。Go 通过<a href="https://tour.golang.org/concurrency/2" target="_blank" rel="noopener">管道</a>支持并发。很多的公司，包括 Google，都在使用 Go，LINE 也用 Go 来开发工具和服务。</p><a id="more"></a><h2 id="Go-GC"><a href="#Go-GC" class="headerlink" title="Go GC"></a>Go GC</h2><p>用 Go，你可以很容易地创建出低延时的应用。Go GC 似乎比其他语言的运行时要简单得多。对于 <a href="https://golang.org/doc/go1.10" target="_blank" rel="noopener">Go 1.10</a> 版本，它的垃圾收集器是 Concurrent Mask &amp; Sweep (CMS) ，它不是压缩的，也不是分代的。这一点跟 JVM 不同。</p><ul><li>它是一个，并行标记，用一个写屏障（写的时候阻塞）的清理（程序）。它是非分代，非压缩的。 – <a href="https://github.com/golang/go/blob/4d7cf3fedbc382215df5ff6167ee9782a9cc9375/src/runtime/mgc.go" target="_blank" rel="noopener">mgc.go</a></li></ul><p>下面是 Java GC 和 Go GC 的对比。相比于 Java ，Go GC 对于我来说看起来有点简单，所以我决定深入进去，看下 Go GC 是怎么工作的。</p><table><thead><tr><th align="left"></th><th align="left">Java(Java8 HotSpot VM)</th><th align="left">Go</th></tr></thead><tbody><tr><td align="left">Collector</td><td align="left">Several collectors (Serial, Parallel, CMS, G1)</td><td align="left">CMS</td></tr><tr><td align="left">Compaction</td><td align="left">Compacts</td><td align="left">Does not compact</td></tr><tr><td align="left">Generational GC</td><td align="left">Generational GC</td><td align="left">Non-generational GC</td></tr><tr><td align="left">Tuning parameters</td><td align="left">Depends on the collector. Multiple parameters available.</td><td align="left">GOGC only</td></tr></tbody></table><h2 id="压缩（Compaction）"><a href="#压缩（Compaction）" class="headerlink" title="压缩（Compaction）"></a>压缩（Compaction）</h2><p>垃圾收集可以选择不迁移或者迁移（堆上的对象）。</p><h2 id="不迁移的-GC（Non-moving-GC）"><a href="#不迁移的-GC（Non-moving-GC）" class="headerlink" title="不迁移的 GC（Non-moving GC）"></a>不迁移的 GC（Non-moving GC）</h2><p>不迁移的垃圾收集不会在堆中给对象重新分配内存。CMS，Go 使用的收集器，就是非迁移的。一般来说，如果你在非迁移的垃圾收集器中，重复地进行内存分配跟释放，最终将导致堆碎片，从而降低分配（堆内存）的性能。但，当然，这也取决于你的内存分配器如何实现。</p><p><img src="https://engineering.linecorp.com/image/2018/8/2/1533208430578.png" alt=""></p><h2 id="迁移的-GC（moving-GC）"><a href="#迁移的-GC（moving-GC）" class="headerlink" title="迁移的 GC（moving GC）"></a>迁移的 GC（moving GC）</h2><p>移动垃圾收集器将活动对象移动到堆的末尾来压缩堆。移动垃圾收集器的一个实例是拷贝 GC（Copying GC），它在 HotSpot VM 中使用。</p><p><img src="https://engineering.linecorp.com/image/2018/8/2/1533208420073.png" alt=""></p><p>压缩具有如下优点：</p><ul><li>避免碎片</li><li>依靠于压缩的分配，可以实现一个高性能的内存分配器（因为所有对象都位于堆的末尾，所以我们可以在右边，最后的位置，增加新的内存。）</li></ul><p><img src="https://engineering.linecorp.com/image/2018/8/2/1533208398145.png" alt=""></p><h2 id="为什么-Go-不选择压缩的方式（Why-Go-did-not-opt-for-compaction）"><a href="#为什么-Go-不选择压缩的方式（Why-Go-did-not-opt-for-compaction）" class="headerlink" title="为什么 Go 不选择压缩的方式（Why Go did not opt for compaction）"></a>为什么 Go 不选择压缩的方式（Why Go did not opt for compaction）</h2><p>来自 Google 的 Rick Hudson，在国际内存管理研讨会（ISMM）上，在他的 keynote 中分享到，<a href="https://blog.golang.org/ismmkeynote" target="_blank" rel="noopener">Getting To Go</a>。</p><ul><li>在 2014 年，他们最初计划做一个任意读的并行拷贝 GC。</li><li>由于没有时间 - 彼时，他们正在将用 C 写的运行时改成用 Go 实现（对运行时的修改） - 他们决定选择 CMS。</li><li>他们采用了基于 TCMalloc 的内存分配器，解决碎片和优化（内存）分配的问题。</li></ul><p>了解更多 Go 内存分配的内容，请看<a href="https://golang.org/doc/go1.4#runtime" target="_blank" rel="noopener">运行时</a>的评论。</p><ul><li>这最初基于 <a href="http://goog-perftools.sourceforge.net/doc/tcmalloc.html" target="_blank" rel="noopener">tcmalloc</a>，但已经修改了很多的地方。- <a href="https://github.com/golang/go/blob/release-branch.go1.10/src/runtime/malloc.go#L7" target="_blank" rel="noopener">malloc.go</a></li></ul><h2 id="分代的-GC（Generational-GC）"><a href="#分代的-GC（Generational-GC）" class="headerlink" title="分代的 GC（Generational GC）"></a>分代的 GC（Generational GC）</h2><p>分代 GC 的目的是通过将堆中对象除以它的年龄（他们从 GC 中存活的次数）来优化 GC，从而产生分代。分代假说指出，在许多应用中，新事物大多年轻。基于该假设，通过以下策略来 （优化）GC，也就是说，我们可以取消多次对旧对象的扫描。</p><ul><li>从年轻的空间（Minor GC）中更频繁地收集垃圾。</li><li>可以将它们，在空间中已经存活了几个 GC 周期的旧对象，重新放置在不经常收集垃圾的空间（Major GC）中</li></ul><p>Java8 HotSpot Vm 的所有收集器都实现了分代 GC。</p><h2 id="写入屏障（Write-barrier）"><a href="#写入屏障（Write-barrier）" class="headerlink" title="写入屏障（Write barrier）"></a>写入屏障（Write barrier）</h2><p>分代 GC 的缺点是，即使垃圾收集没有运行，对于应用程序也有开销。我们来看一个 Minor GC 的例子。</p><p><img src="https://engineering.linecorp.com/image/2018/8/2/1533208408997.png" alt=""></p><p>如果我们仅检查 root 用于指向 Minor 的指针，然后收集无法访问的对象，那么旧对象中引用新对象（如图中的 obj2）会被意外地收集。但是，如果我们检查整个堆，包括旧对象以避免收集 Minor 对象（时产生的问题），那么对于分代 GC 来说就没有意义。因此，添加一个进程，以便在替换或重写引用时将旧对象的引用记录保存到新对象中。我们将此额外流程称为写入屏障。使用分代 GC 可能有更多好处，可以弥补这个缺点（写入屏障开销）。</p><h2 id="为什么不使用分代-GC？（Why-not-generational-GC-）"><a href="#为什么不使用分代-GC？（Why-not-generational-GC-）" class="headerlink" title="为什么不使用分代 GC？（Why not generational GC?）"></a>为什么不使用分代 GC？（Why not generational GC?）</h2><p>正如我们之前看到的，分代垃圾收集器需要一个写屏障来记录代之间的指针。回到 Rick Hudson 的主题演讲，Getting To Go，我们可以看到他们确实考虑过分代 GC，但由于写屏障开销而放弃了它。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">写屏障速度很快，但简单来说，它还不够快。</span><br></pre></td></tr></table></figure><p>使用 Go，编译器的逃逸分析非常出色，如果需要，程序员可以控制到，不在堆上分配对象，因此短期对象通常分配在栈中而不是在堆中；这意味着不需要 GC。总的来说，你从分代 GC 得到的（好处）比其他（语言）运行时少。有一些用 Go 语言编写的库，跟速度一样出名的是，这些库恰好也是零内存分配。尽管如此，我们仍然有消耗，在每次 GC 循环中多次扫描长寿命的对象。来自 Google 的 Ian Lance Taylor 已经在 golang-nuts 中提到了这一点，<a href="https://groups.google.com/forum/#!topic/golang-nuts/KJiyv2mV2pU" target="_blank" rel="noopener">为什么垃圾收集器不实现分代 GC 功能？</a></p><ul><li>这是个很好的问题。Go 当前的 GC 显然做了一些额外的工作，但它也跟其他的工作并行执行，所以在具有备用 CPU 的系统上，Go 正在作出合理的选择。请看 <a href="https://golang.org/issue/17969" target="_blank" rel="noopener">https://golang.org/issue/17969</a></li></ul><h2 id="结束语（Closing-notes）"><a href="#结束语（Closing-notes）" class="headerlink" title="结束语（Closing notes）"></a>结束语（Closing notes）</h2><p>通过研究 Go 垃圾收集器，我能够理解 Go GC 当前结构的背景以及它如何克服它的弱点。Go 发展得非常快。如果你对 Go 感兴趣，最好继续留意它（当我写这篇文章时，2018 年 8 月，Go 发布了它的 1.11 版本）。</p><hr><p>via: <a href="https://engineering.linecorp.com/en/blog/detail/342" target="_blank" rel="noopener">https://engineering.linecorp.com/en/blog/detail/342</a></p><p>作者：<a href="https://twitter.com/ocadaruma" target="_blank" rel="noopener">Haruki Okada</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;大家好，我是 Okada（&lt;a href=&quot;https://twitter.com/ocadaruma&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@ocadaruma&lt;/a&gt;）,LINE 广告平台团队的成员。我对 Go 的 GC （垃圾收集器）有点感兴趣，甚至还促使我专门写一篇关于它的博客。Go 是一门由 Google 开发，并且支持垃圾收集的编程语言。Go 通过&lt;a href=&quot;https://tour.golang.org/concurrency/2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;管道&lt;/a&gt;支持并发。很多的公司，包括 Google，都在使用 Go，LINE 也用 Go 来开发工具和服务。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】在 Go 运行时中的 Strings</title>
    <link href="http://yoursite.com/2018/10/08/%E3%80%90%E8%AF%91%E3%80%91%E5%9C%A8-Go-%E8%BF%90%E8%A1%8C%E6%97%B6%E4%B8%AD%E7%9A%84-Strings/"/>
    <id>http://yoursite.com/2018/10/08/%E3%80%90%E8%AF%91%E3%80%91%E5%9C%A8-Go-%E8%BF%90%E8%A1%8C%E6%97%B6%E4%B8%AD%E7%9A%84-Strings/</id>
    <published>2018-10-08T14:04:05.000Z</published>
    <updated>2020-05-02T14:09:50.160Z</updated>
    
    <content type="html"><![CDATA[<h2 id="动机（Movivation）"><a href="#动机（Movivation）" class="headerlink" title="动机（Movivation）"></a>动机（Movivation）</h2><p>可以认为这是对 <a href="https://ai.google/research/people/r" target="_blank" rel="noopener">Rob Pike</a> 的一篇关于 Go 字符串的<a href="https://blog.golang.org/strings" target="_blank" rel="noopener">博客</a>的解释。以 Go 程序员的思维来写。</p><a id="more"></a><p>在博客文章中，他说没有什么证据可以证明 Go 字符串是以 slices 的方式实现。我猜这不是他的疏忽，但起码可以证明一点，这不是这篇博客的核心。再说，任何想要自己研究的都可以在使用 Go 代码的过程中探索。我就是这样做的。所以在这篇文章中，我尽力解释 Go 字符串实现与 slice 实现的关系。</p><p>这里不讨论字符串编码。这不是在 runtime 中我想要探索的东西。但我模仿 slice 的常见字符串操作，如字符串的长度（len(“go”)），串联（”go” + “lang”），索引（”golang”[0]）和切片（”golang”[0:2])。老实说，索引或者切片是在他们自己的内部范围中的操作，这意味着，他们在字符串上的可用性与字符串上的性质没有任何关系（或很少）。这可能并不完全正确，但请（先）接受它，因为事实上，这将通过底层所谓的基本类型返回，把我们带入到 Go 编译器中。再说了，这篇文章又不是我的誓言（我需要对所说的一切负责）。</p><h2 id="字符串的本质（The-Nature-of-Strings）"><a href="#字符串的本质（The-Nature-of-Strings）" class="headerlink" title="字符串的本质（The Nature of Strings）"></a>字符串的本质（The Nature of Strings）</h2><p>我还没有遇到一种编程语言，其中的字符串具有不同的底层内存结构：内存为连续的槽位。这意味着字符串的字节在内存中都是彼此相邻，两者之间没有其他的东西。也就是说，如果你在程序中使用了著名，12 个字节的字符串：”hello, world”，并且有机会在内存中检查他们，你会发现他们位于同一行，每个字节（或字符）紧跟着另一个，他们之间没有空格或外来的字节。据我所知，Go 并没有偏离这个原则。</p><p>但这都是关于内存，物理上的东西。当我们站在这个角度上看，所有事情都是相同的，也就是说编程语言之间的差异不见了。因此，让我们返回运行时的上一层，在这里我们将看到不同语言如何处理它们的业务，我们也将在这里找到 Go 是如何实现它的字符串数据的细节。幸运的是，Go 运行时的一个重要部分是用 Go 编写的，感谢上帝的保佑（不然我得研究个半死），（已存在的）大量的讨论已经解释了明显和不那么明显的实现细节。在编写本文时，可以在 Github 上找到运行时字符串的实现。让我们一起走近它吧。</p><h2 id="Go-字符串（Go’s-String）"><a href="#Go-字符串（Go’s-String）" class="headerlink" title="Go 字符串（Go’s String）"></a>Go 字符串（Go’s String）</h2><p>在 Go 运行时，字符串是 stringStruct 类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><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> stringStruct <span class="keyword">struct</span> &#123;</span><br><span class="line">  str unsafe.Pointer</span><br><span class="line">  <span class="built_in">len</span> <span class="keyword">int</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>它由一个 str、指向实际字节所在的内存块的指针和 len（字符串的长度）组成。由于字符串是不可变的，因此前面说的这些不会改变。</p><h2 id="创建一个新的字符串（Creating-a-New-String）"><a href="#创建一个新的字符串（Creating-a-New-String）" class="headerlink" title="创建一个新的字符串（Creating a New String）"></a>创建一个新的字符串（Creating a New String）</h2><p>负责在运行时创建新字符串的函数为 rawstring。以下是它具体的实现（代码中的注释是我的）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">rawstring</span><span class="params">(size <span class="keyword">int</span>)</span> <span class="params">(s <span class="keyword">string</span>, b []<span class="keyword">byte</span>)</span></span> &#123;</span><br><span class="line">  <span class="comment">// 1. 分配符合字符串大小的内存块，并返回指针给它：</span></span><br><span class="line">  p := mallocgc(<span class="keyword">uintptr</span>(size), <span class="literal">nil</span>, <span class="literal">false</span>)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. 用刚返回的指针创建一个元数据（stringStruct），并指定该字符串的大小。</span></span><br><span class="line">  stringStructOf(&amp;s).str = p</span><br><span class="line">  stringStructOf(&amp;s).<span class="built_in">len</span> = size</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 3. 准备一个字节类型的切片，实际上会将字符串数据存储在这里。</span></span><br><span class="line">  *(*slice)(unsafe.Pointer(&amp;b)) = slice&#123;p, size, size&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>rawstring 返回一个字符串和一个字节切片，其中应该存储字符串的实际字节，并且这个字节切片将用于字符串的所有操作。我们可以安全地将它们称为数据（[]byte）和元数据（stringStruct）。</p><p>但这并不是结束。也许这是唯一一次，你需要研究字符串里面实际的非零字节切片。事实上，对 rawstring 的注释已经提示调用者，它只使用一次字节切片（写入字符串的内容），然后就删除它。其余的时间，字符串结构本身就已经足够了。</p><p>知道了这一点，让我们看看如何实现一些常见的字符串操作。这对我们也是有意义的，这里将会介绍为什么不建议大家通过旧的串连来构建大字符串。</p><h2 id="相同的字符串操作（Common-String-Operations）"><a href="#相同的字符串操作（Common-String-Operations）" class="headerlink" title="相同的字符串操作（Common String Operations）"></a>相同的字符串操作（Common String Operations）</h2><h3 id="长度（len-“go”-）"><a href="#长度（len-“go”-）" class="headerlink" title="长度（len(“go”)）"></a>长度（len(“go”)）</h3><p>由于字符串是不可变的，也就是说字符串的长度将保持不变。其实，当我们存储字符串时，我们就已经知道这就是我们存储在 stringStruct 的 len 字段中的内容。因此，无论字符串的大小如何，对字符串长度的请求都花费相同的时间。在 Big-O 术语中，它是一个 O(1) 操作。</p><h3 id="串联（”go”-“lang”）-Concatenation-“go”-“lang”"><a href="#串联（”go”-“lang”）-Concatenation-“go”-“lang”" class="headerlink" title="串联（”go” + “lang”）(Concatenation (“go” + “lang”))"></a>串联（”go” + “lang”）(Concatenation (“go” + “lang”))</h3><p>这是一个简单的过程。Go 首先通过对要连接的所有字符串的长度求和来确定结果字符串的长度。然后它请求连续内存块的大小。（这里）有优化检查，更重要的是安全检查。安全检查确保结果字符串的长度不超过 Go 的最大整数值。</p><p>然后，该过程的下一步开始。各个字符串的字节将一个接一个地复制到新字符串中。也就是说，存储器中不同位置的字节被复制到新位置。这项工作不是很合理，应该尽可能避免。因此建议使用 strings.Builder，因为它最小化了内存复制。这将使我们的性能最接近可变字符串。</p><h3 id="索引-“golang”-0-（Indexing-“golang”-0-）"><a href="#索引-“golang”-0-（Indexing-“golang”-0-）" class="headerlink" title="索引(“golang”[0])（Indexing (“golang”[0])）"></a>索引(“golang”[0])（Indexing (“golang”[0])）</h3><p>Go 的索引运算符为 [index]，其中 index 是一个整数。在撰写本文时，它可用于数据，切片，字符串和一些指针。</p><p>什么是数组，切片和字符串具有的共同的基础类型？在物理内存方面，它是一个连续的内存块。在 Go 用语中，是一个数组。对于字符串，这是 rawstring 返回的字节切片，它是存储字符串内容的位置。也就是我们的索引。不言而喻，我上面提到的与索引操作符兼容的 “一些指针” 是具有数组底层类型的那些。</p><p>请注意，它在 map 上的语法相同，但行为不同。对于 map，键的类型确定了括号之间的值的类型。</p><h3 id="切片（”golang”-0-2-）-Slicing-“golang”-0-2"><a href="#切片（”golang”-0-2-）-Slicing-“golang”-0-2" class="headerlink" title="切片（”golang”[0:2]）(Slicing (“golang”[0:2]))"></a>切片（”golang”[0:2]）(Slicing (“golang”[0:2]))</h3><p>slice 运算符与索引运算符具有相同的兼容性：操作数必须具有基础类型的数组。因此它适用于同一组类型：数组，切片，字符串和一些指针。</p><p>在字符串上有一个警告。完整切片运算符为[low:high:capacity]。一次性它允许您创建切片并设置底层数组的容量。但是请记住字符串是不可变的，因此永远不需要（分配）基础数组大于字符串内容所需的字节。也因此，字符串不存在切片运算符。</p><h3 id="strings-和-strconv-包（The-strings-and-strconv-packages）"><a href="#strings-和-strconv-包（The-strings-and-strconv-packages）" class="headerlink" title="strings 和 strconv 包（The strings and strconv packages）"></a>strings 和 strconv 包（The strings and strconv packages）</h3><p>Go 提供了用于处理字符串的 string 和 strconv 包。我已经提到了用于构建大字符串的更高效的 Builder。它由 strings 包提供。还有其他的细节。他们一起为字符串转换，比较，搜索和替换等提供调整功能。它会在构建自己的字符串之前检查它们。</p><h3 id="误解的根源（Source-of-Confusion）"><a href="#误解的根源（Source-of-Confusion）" class="headerlink" title="误解的根源（Source of Confusion）"></a>误解的根源（Source of Confusion）</h3><h4 id="cap-slice-vs-cap-string"><a href="#cap-slice-vs-cap-string" class="headerlink" title="cap(slice) vs cap(string)"></a>cap(slice) vs cap(string)</h4><p>内置函数 cap 返回切片底层数组的容量。在切片的整个生命周期中，底层阵列的容量可以不断变化。通常它会增长以容纳新元素。如果字符串是切片，为什么它不能返回 cap 查询？答案很简单：Go 字符串是不可变的。也就是说，它的大小永远不会增长或缩小，这反过来意味着如果实现了 cap，它将与 len 相同。</p><hr><p>via: <a href="https://boakye.yiadom.org/go/strings/" target="_blank" rel="noopener">https://boakye.yiadom.org/go/strings/</a></p><p>作者：<a href="https://github.com/yawboakye" target="_blank" rel="noopener">Yaw Boakye</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/Unknwon" target="_blank" rel="noopener">Unknwon</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;动机（Movivation）&quot;&gt;&lt;a href=&quot;#动机（Movivation）&quot; class=&quot;headerlink&quot; title=&quot;动机（Movivation）&quot;&gt;&lt;/a&gt;动机（Movivation）&lt;/h2&gt;&lt;p&gt;可以认为这是对 &lt;a href=&quot;https://ai.google/research/people/r&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rob Pike&lt;/a&gt; 的一篇关于 Go 字符串的&lt;a href=&quot;https://blog.golang.org/strings&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;博客&lt;/a&gt;的解释。以 Go 程序员的思维来写。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 功能：将 Go 程序员带向极限</title>
    <link href="http://yoursite.com/2018/06/29/%E3%80%90%E8%AF%91%E3%80%91Go-%E5%8A%9F%E8%83%BD%EF%BC%9A%E5%B0%86-Go-%E7%A8%8B%E5%BA%8F%E5%91%98%E5%B8%A6%E5%90%91%E6%9E%81%E9%99%90/"/>
    <id>http://yoursite.com/2018/06/29/%E3%80%90%E8%AF%91%E3%80%91Go-%E5%8A%9F%E8%83%BD%EF%BC%9A%E5%B0%86-Go-%E7%A8%8B%E5%BA%8F%E5%91%98%E5%B8%A6%E5%90%91%E6%9E%81%E9%99%90/</id>
    <published>2018-06-29T14:02:35.000Z</published>
    <updated>2020-05-02T14:11:54.918Z</updated>
    
    <content type="html"><![CDATA[<h2 id="功能范式（The-Functional-Paradigm）"><a href="#功能范式（The-Functional-Paradigm）" class="headerlink" title="功能范式（The Functional Paradigm）"></a>功能范式（The Functional Paradigm）</h2><p>函数式编程基于简单的规则，目的是使程序保持一致，使并行（更）人性化和简单化，函数是存粹的，它不会改变（变量的）状态，不会改变对象，也不共享状态，代码是声明式的，计算只不过是数学函数的一种实现。</p><a id="more"></a><p>我们来看一个纯粹的函数语言的函数例子：</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">isPalindrome :: (Eq a) &#x3D;&gt; [a] -&gt; Bool</span><br><span class="line">isPalindrome x &#x3D; x &#x3D;&#x3D; reverse x</span><br></pre></td></tr></table></figure><p>上面的代码是 haskell 版本的 palindrome 函数，我们（实际上）都在某种程度上通过循环来遍历列表，以及检查…，在 Haskell 中，isPalindrome 函数获取 Equality 的字符列表（考虑比较时），并返回一个 bool 值（True 或者 False）。实现（这个功能）只需一行代码：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x &#x3D;&#x3D; reverse x</span><br></pre></td></tr></table></figure><p>将参数和 reverse 输出进行比较，reverse 是一个反转列表的函数。</p><p>我们来看如何在 Golang 中实现一个相似的函数，从一个递归版本的 reverse 开始吧：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">reverse</span><span class="params">(str <span class="keyword">string</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line"><span class="keyword">if</span> str == <span class="string">""</span> &#123;</span><br><span class="line"><span class="keyword">return</span> str</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">return</span> reverse(str[<span class="number">1</span>:]) + <span class="keyword">string</span>(str[<span class="number">0</span>]) <span class="comment">// a string is a byte array in golang</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在我们有了一个名为 reverse 的函数，让我们再看一下如何实现 isPalindrome ，用类似于函数的方式。</p><figure class="highlight go"><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">isPalindrome</span> <span class="params">(str <span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> str == reverse(str)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就像你看到的，我们最终用几行代码就写了一个大家一看就懂的函数，在大多数情况下，非常有名的函数都可以实现 go 语言的版本，函数在 golang 中是第一公民，所以我们可以做一些类似下面的实现：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">isPalindrome := <span class="function"><span class="keyword">func</span> <span class="params">(str <span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> str == reverse(str)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">isPalindrome(<span class="string">"radar"</span>)</span><br></pre></td></tr></table></figure><p>这只是 golang 功能中很小的一部分，还有很多 go 语言实现中有大量优化的地方的例子，例如 go 中的 map，reduce，和 filter，一个好的实践方法是优化它们，并使用它们。</p><p>例如，数据处理在 golang 中的速度要比 python 快得多，所以，（虽然）我（都）可以创建一个数据管道来清理和组织我的数据，但如果我愿意用 golang 实现的话，将更快。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> p <span class="function"><span class="keyword">func</span><span class="params">(<span class="keyword">int</span>)</span> <span class="title">int</span></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">apInt</span><span class="params">(functions []p, numbers []<span class="keyword">int</span>)</span> []<span class="title">int</span></span>&#123;</span><br><span class="line">j := <span class="number">0</span></span><br><span class="line">    output := <span class="built_in">make</span>([]<span class="keyword">int</span>,<span class="number">0</span>)</span><br><span class="line"><span class="keyword">for</span> _,f := <span class="keyword">range</span> functions &#123;</span><br><span class="line"><span class="keyword">for</span> j &lt; <span class="built_in">len</span>(numbers) &#123;</span><br><span class="line">fmt.Println(f(numbers[j]))</span><br><span class="line">output = <span class="built_in">append</span>(output,f(numbers[j]))</span><br><span class="line">            j++</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">    <span class="keyword">return</span> output</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的 functions，是一个类似于如下声明的函数数组：</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">listOfFuncs := []p&#123;a, b&#125;</span><br></pre></td></tr></table></figure><p>它能够在所有的整型切片中工作，并且没有副作用。</p><p>我刚为 <a href="https://github.com/radicalrafi/lori" target="_blank" rel="noopener">lori</a> 工作，它是一个 Golang 库，目标是为开发者提供这些有用的东西，并使 function 变得有趣和可能。</p><hr><p>via: <a href="https://radicalrafi.github.io/posts/functional-go/" target="_blank" rel="noopener">https://radicalrafi.github.io/posts/functional-go/</a></p><p>作者：<a href="https://github.com/radicalrafi" target="_blank" rel="noopener">radicalrafi </a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/校对者ID" target="_blank" rel="noopener">校对者ID</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;功能范式（The-Functional-Paradigm）&quot;&gt;&lt;a href=&quot;#功能范式（The-Functional-Paradigm）&quot; class=&quot;headerlink&quot; title=&quot;功能范式（The Functional Paradigm）&quot;&gt;&lt;/a&gt;功能范式（The Functional Paradigm）&lt;/h2&gt;&lt;p&gt;函数式编程基于简单的规则，目的是使程序保持一致，使并行（更）人性化和简单化，函数是存粹的，它不会改变（变量的）状态，不会改变对象，也不共享状态，代码是声明式的，计算只不过是数学函数的一种实现。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言中的线程池</title>
    <link href="http://yoursite.com/2018/05/27/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    <id>http://yoursite.com/2018/05/27/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0/</id>
    <published>2018-05-27T13:04:15.000Z</published>
    <updated>2020-05-02T14:11:41.927Z</updated>
    
    <content type="html"><![CDATA[<p>用过一段时间的 Go 之后，我学会了如何使用一个不带缓存的 channel 去创建一个 goroutine 池。我喜欢这个实现，这个实现甚至比这篇博文描述的更好。虽然这样说，这篇博文仍然对它所描述的部分有一定的价值。</p><a id="more"></a><p><a href="https://github.com/goinggo/work" target="_blank" rel="noopener">https://github.com/goinggo/work</a></p><h2 id="介绍（Introduction）"><a href="#介绍（Introduction）" class="headerlink" title="介绍（Introduction）"></a>介绍（Introduction）</h2><p>在我的服务器开发的职业生涯里，线程池一直是在微软系统的堆栈上构建健壮代码的关键。微软在 .Net 上的失败，是因为它给每个进程分配一个单独的线程池，并认为在它们并发运行时能够管理好。我早就已经意识到这是不可能的。至少，在我开发的服务器上不可行。</p><p>当我用 Win32 API，C/C++ 构建系统时，我创建了一个抽象的 IOCP 类，它可以给我分配好线程池，我把工作扔给它（去处理）。这样工作得非常好，并且我还能够指定线程池的数量和并发度（能够同时被执行的线程数）。在我使用 C# 开发的时间里，我沿用了这段代码。如果你想了解更多，我在几年前写了一篇文章 (<a href="http://www.theukwebdesigncompany.com/articles/iocp-thread-pooling.php)[http://www.theukwebdesigncompany.com/articles/iocp-thread-pooling.php]。" target="_blank" rel="noopener">http://www.theukwebdesigncompany.com/articles/iocp-thread-pooling.php)[http://www.theukwebdesigncompany.com/articles/iocp-thread-pooling.php]。</a> 使用 IOCP，给我带来了需要的性能和灵活性。 顺便说一下，.Net 线程池使用了下面的 IOCP。</p><p>线程池的想法非常简单。工作被发送到服务器，它们需要被处理。大多数工作本质上是异步的，但不一定是。大多数时候，工作来自于一个内部协程的通信。线程池将工作加入其中，然后这个池子中的一个线程会被分配来处理这个工作。工作按照接收的顺序被执行。线程池为有效地执行工作提供了一个很好的模式。（设想一下，）每次需要处理工作时，产生一个新线程会给操作系统带来沉重的负担，并导致严重的性能问题。</p><p>那么如何调整线程池的性能呢？你需要找出线程池包含多少个线程时，工作被处理得最快。当所有的线程都在忙着处理任务时，新的任务将待在队列里。这是你希望的，因为从某些方面来说，太多的线程（反而）会导致处理工作变得更慢。导致这个现象有几个原因，像机器上的 CPU 核数需要有能力去处理数据库请求（等）。经过测试，你可以找到最合适的数值。</p><p>我总是先找出（机器上的 CPU）有多少个核，以及要被处理的工作的类型。工作阻塞时，（它们）平均被阻塞多久。在微软系统的堆栈上，我发现对于大多数工作来说，每个核上运行 3 个线程能够获得最好的性能。Go 的话，我还不知道最佳的数字。</p><p>你也可以为不同类型的工作创建不同的线程池。因为每种线程池都可以被配置，你可以花点时间使服务器获得最大输出。通过这种方式的指挥和控制对于实现最大化服务器能力至关重要。</p><p>在 Go 语言中我不创建线程，而是创建协程。协程函数类似于多线程函数，但由 Go 来管理实际上在系统层面运行的线程。了解更多关于 Go 中的并发，查看这个文档：<a href="http://golang.org/doc/effective_go.html#concurrency" target="_blank" rel="noopener">http://golang.org/doc/effective_go.html#concurrency</a>。</p><p>我创建了名为 workpool 和 jobpool 的包。它们通过 channel 和 go 协程来实现池的功能。</p><h2 id="工作池（Workpool）"><a href="#工作池（Workpool）" class="headerlink" title="工作池（Workpool）"></a>工作池（Workpool）</h2><p>这个包创建了一个 go 协程池，专门用来处理发布到池子中的工作。一个独立的 Go 协程负责工作的排队处理。协程提供安全的工作排队，跟踪队列中工作量，当队列满时报告错误。</p><p>提交工作到队列中是一个阻塞操作。这样调用者才能知道工作是否已经进入队列。（workpool 也会一直）保持工作队列中活动程序数量的计数。</p><p>这是如何使用 workpool 的样例代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">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">"bufio"</span></span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">    <span class="string">"os"</span></span><br><span class="line">    <span class="string">"runtime"</span></span><br><span class="line">    <span class="string">"strconv"</span></span><br><span class="line">    <span class="string">"time"</span></span><br><span class="line"></span><br><span class="line">    <span class="string">"github.com/goinggo/workpool"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> MyWork <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="keyword">string</span></span><br><span class="line">    BirthYear <span class="keyword">int</span></span><br><span class="line">    WP *workpool.WorkPool</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">(mw *MyWork)</span> <span class="title">DoWork</span><span class="params">(workRoutine <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">"%s : %d\n"</span>, mw.Name, mw.BirthYear)</span><br><span class="line">    fmt.Printf(<span class="string">"Q:%d R:%d\n"</span>, mw.WP.QueuedWork(), mw.WP.ActiveRoutines())</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Simulate some delay</span></span><br><span class="line">    time.Sleep(<span class="number">100</span> * time.Millisecond)</span><br><span class="line">&#125;</span><br><span class="line"></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">    runtime.GOMAXPROCS(runtime.NumCPU())</span><br><span class="line"></span><br><span class="line">    workPool := workpool.New(runtime.NumCPU(), <span class="number">800</span>)</span><br><span class="line"></span><br><span class="line">    shutdown := <span class="literal">false</span> <span class="comment">// Race Condition, Sorry</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">            work := MyWork &#123;</span><br><span class="line">                Name: <span class="string">"A"</span> + strconv.Itoa(i),</span><br><span class="line">                BirthYear: i,</span><br><span class="line">                WP: workPool,</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> err := workPool.PostWork(<span class="string">"routine"</span>, &amp;work); err != <span class="literal">nil</span> &#123;</span><br><span class="line">                fmt.Printf(<span class="string">"ERROR: %s\n"</span>, err)</span><br><span class="line">                time.Sleep(<span class="number">100</span> * time.Millisecond)</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> shutdown == <span class="literal">true</span> &#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"></span><br><span class="line">    fmt.Println(<span class="string">"Hit any key to exit"</span>)</span><br><span class="line">    reader := bufio.NewReader(os.Stdin)</span><br><span class="line">    reader.ReadString(’\n’)</span><br><span class="line"></span><br><span class="line">    shutdown = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">    fmt.Println(<span class="string">"Shutting Down"</span>)</span><br><span class="line">    workPool.Shutdown(<span class="string">"routine"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看下 main 函数，我们创建了一个协程池，协程数量基于机器上的核数。这意味每个核都对应有一个协程。如果每个核都处于忙碌状态，（那么）你将无法做更多的事情。再（运行）一次，性能测试会检测出哪个数量是最合适的。第二个参数是队列的大小。在这种情况下，我让队列足够大（800），保证所有的请求都可以进来。</p><p>MyWork 类型定义了我需要执行的工作状态。我们需要成员函数 DoWork，因为它实现了 PostWork 调用的接口。要将任何任务传递给线程池，都必须实现这个方法。</p><p>DoWork 方法做了两件事。第一是，它显示对象的状态。第二，它实时报告队列中的数量和 Go 协程并发执行的数量。这些数值可以用来检查线程池的健康状态和做性能测试。</p><p>最后，一个 Go 协程专门循环地将工作传递给工作池。同时，工作池为队列中的每个对象执行 DoWork 方法。Go 协程最终会完成，工作池继续执行它的工作。在任何时候当我们介入时，程序将优雅地停止。</p><p>在这个范例程序中，PostWork 方法能够返回一个错误。这是因为 PostWork 方法将保证任务放在队列中或者失败。这个失败的唯一原因是队列已满。（所以）设置队列的长度是一个重要的考虑项。</p><h2 id="作业池（Jobpool）"><a href="#作业池（Jobpool）" class="headerlink" title="作业池（Jobpool）"></a>作业池（Jobpool）</h2><p>jobpool 包跟 workpool 包很相似，除了一个实现的细节。这个包包含两个队列，一个是普通的处理队列，另外一个是高优先级的处理队列。阻塞的高优先级队列总是比阻塞的普通队列先获得处理。</p><p>两种队列的使用导致 jobpool 比 workpool 更加复杂。如果你不需要高优先级的处理，那么使用 workpool 将更快，更有效。</p><p>这是如何使用 jobpool 的范例代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">    <span class="string">"time"</span></span><br><span class="line"></span><br><span class="line">    <span class="string">"github.com/goinggo/jobpool"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> WorkProvider1 <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(wp *WorkProvider1)</span> <span class="title">RunJob</span><span class="params">(jobRoutine <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">"Perform Job : Provider 1 : Started: %s\n"</span>, wp.Name)</span><br><span class="line">    time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">    fmt.Printf(<span class="string">"Perform Job : Provider 1 : DONE: %s\n"</span>, wp.Name)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> WorkProvider2 <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(wp *WorkProvider2)</span> <span class="title">RunJob</span><span class="params">(jobRoutine <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">"Perform Job : Provider 2 : Started: %s\n"</span>, wp.Name)</span><br><span class="line">    time.Sleep(<span class="number">5</span> * time.Second)</span><br><span class="line">    fmt.Printf(<span class="string">"Perform Job : Provider 2 : DONE: %s\n"</span>, wp.Name)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    jobPool := jobpool.New(<span class="number">2</span>, <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line">    jobPool.QueueJob(<span class="string">"main"</span>, &amp;WorkProvider1&#123;<span class="string">"Normal Priority : 1"</span>&#125;, <span class="literal">false</span>)</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">"*******&gt; QW: %d AR: %d\n"</span>,</span><br><span class="line">        jobPool.QueuedJobs(),</span><br><span class="line">        jobPool.ActiveRoutines())</span><br><span class="line"></span><br><span class="line">    time.Sleep(<span class="number">1</span> * time.Second)</span><br><span class="line"></span><br><span class="line">    jobPool.QueueJob(<span class="string">"main"</span>, &amp;WorkProvider1&#123;<span class="string">"Normal Priority : 2"</span>&#125;, <span class="literal">false</span>)</span><br><span class="line">    jobPool.QueueJob(<span class="string">"main"</span>, &amp;WorkProvider1&#123;<span class="string">"Normal Priority : 3"</span>&#125;, <span class="literal">false</span>)</span><br><span class="line"></span><br><span class="line">    jobPool.QueueJob(<span class="string">"main"</span>, &amp;WorkProvider2&#123;<span class="string">"High Priority : 4"</span>&#125;, <span class="literal">true</span>)</span><br><span class="line">    fmt.Printf(<span class="string">"*******&gt; QW: %d AR: %d\n"</span>,</span><br><span class="line">        jobPool.QueuedJobs(),</span><br><span class="line">        jobPool.ActiveRoutines())</span><br><span class="line"></span><br><span class="line">    time.Sleep(<span class="number">15</span> * time.Second)</span><br><span class="line"></span><br><span class="line">    jobPool.Shutdown(<span class="string">"main"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个范例代码中，我们创建了两个 worker 类型的结构体。可以将每个 worker 都视为系统中一个独立的作业。</p><p>在 main 函数中，我们创建了一个包含 2 个协程的作业池，支持 1000 个待处理的作业。首先我们创建了 3 个不同的 WorkProvider1 对象，并将她们传递给了队列，设置优先级标志位为 false。接下来我们创建一个 WorkProvider2 对象，并将它传递给队列，设置优先级标志位为 true。</p><p>因为作业池中有 2 个协程，先创建的两个作业将进入队列并被处理。一旦它们的任务完成，接下来的作业将从队列中检索。WorkProvider2 作业将会被执行，因为它被放在了高优先级队列中。</p><p>想获取 workpool 包和 jobpool 包的代码，请访问 <a href="github.com/goinggo">github.com/goinggo</a></p><p>一如既往，我希望这份代码可以在某些方面帮上你一点点。</p><hr><p>via: <a href="https://www.ardanlabs.com/blog/2013/05/thread-pooling-in-go-programming.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2013/05/thread-pooling-in-go-programming.html</a></p><p>作者：<a href="https://github.com/ardanlabs/gotraining" target="_blank" rel="noopener">William Kennedy</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/校对者ID" target="_blank" rel="noopener">校对者ID</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;用过一段时间的 Go 之后，我学会了如何使用一个不带缓存的 channel 去创建一个 goroutine 池。我喜欢这个实现，这个实现甚至比这篇博文描述的更好。虽然这样说，这篇博文仍然对它所描述的部分有一定的价值。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】创建和使用 HTTP 中间层</title>
    <link href="http://yoursite.com/2018/04/30/%E3%80%90%E8%AF%91%E3%80%91%E5%88%9B%E5%BB%BA%E5%92%8C%E4%BD%BF%E7%94%A8-HTTP-%E4%B8%AD%E9%97%B4%E5%B1%82/"/>
    <id>http://yoursite.com/2018/04/30/%E3%80%90%E8%AF%91%E3%80%91%E5%88%9B%E5%BB%BA%E5%92%8C%E4%BD%BF%E7%94%A8-HTTP-%E4%B8%AD%E9%97%B4%E5%B1%82/</id>
    <published>2018-04-30T14:01:26.000Z</published>
    <updated>2020-05-02T14:08:57.895Z</updated>
    
    <content type="html"><![CDATA[<p>在构建 Web 应用时，可能需要为许多（甚至全部）HTTP 请求运行一些公共的函数。你可能需要对每个请求进行记录，对每个响应进行压缩，或者在执行一些重要的处理之前检查一下缓存。</p><a id="more"></a><p>组织这种公共函数的一种方法是将其设置为中间层 - 自包含代码，它们在正常应用处理程序之前或之后，独立地处理请求。在 Go 中，使用中间层的常见位置在 ServeMux 和应用程序处理之间，因此通常对 HTTP 请求的控制流程如下所示：</p><p><code>ServeMux =&gt; Middleware Handler =&gt; Application Handler</code></p><p>在这篇文章中，我将解释如何使自定义中间层在这种模式下工作，以及如何使用第三方中间层包的一些具体的示例。</p><h2 id="基本原则（The-Basic-Principles）"><a href="#基本原则（The-Basic-Principles）" class="headerlink" title="基本原则（The Basic Principles）"></a>基本原则（The Basic Principles）</h2><p>在 Go 中制作和使用中间层很简单。我们可以设想：</p><p>实现我们自己的中间层，使其满足 http.Handler 接口。<br>构建一个包含我们的中间层处理程序和我们的普通应用处理程序的处理链，我们可以使用它来注册 http.ServeMux。我会解释如何做。</p><p>希望你已经熟悉下面构造一个处理程序的方法（如果没有，最好在继续阅读前，看下这个底层的程序）。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">messageHandler</span><span class="params">(message <span class="keyword">string</span>)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">    w.Write([]<span class="keyword">byte</span>(message)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个处理程序中，我们将我们的逻辑（一个简单的 <code>w.Write</code>）放在匿名函数中，并封装 <code>message</code> 变量以形成闭包。然后我们使用 http.HandlerFunc 适配器并将其返回，将此闭包转换为处理程序。</p><p>我们可以使用这种相同的方法来创建一系列的处理程序。我们将链中的下一个处理程序作为变量传递给闭包（而不是像上面那样），然后通过调用 ServeHTTP() 方法将控制权转移给下一个处理程序。</p><p>这为我们提供了构建中间层的完整的模式：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">exampleMiddleware</span><span class="params">(next http.Handler)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">    <span class="comment">// Our middleware logic goes here...</span></span><br><span class="line">    next.ServeHTTP(w, r)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你会注意到这个中间层函数有一个  <code>func (http.Handler) http.Handler</code> 签名。它接受一个处理程序作为参数并返回一个处理程序。这是有用的，原因有两个：</p><p>因为它返回一个处理程序，我们可以直接使用 net/http 软件包提供的标准 ServeMux 注册中间层函数。<br>通过将中间层函数嵌套在一起，我们可以创建一个任意长的处理程序链。例如：</p><p><code>http.Handle(&quot;/&quot;, middlewareOne(middlewareTwo(finalHandler)))</code></p><h2 id="控制流程说明（Illustrating-the-Flow-of-Control）"><a href="#控制流程说明（Illustrating-the-Flow-of-Control）" class="headerlink" title="控制流程说明（Illustrating the Flow of Control）"></a>控制流程说明（Illustrating the Flow of Control）</h2><p>让我们看一个简单的例子，它带有一些只需将日志消息写入标准输出的中间层函数：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">File: main.go</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"log"</span></span><br><span class="line">  <span class="string">"net/http"</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">middlewareOne</span><span class="params">(next http.Handler)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">    log.Println(<span class="string">"Executing middlewareOne"</span>)</span><br><span class="line">    next.ServeHTTP(w, r)</span><br><span class="line">    log.Println(<span class="string">"Executing middlewareOne again"</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">middlewareTwo</span><span class="params">(next http.Handler)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">    log.Println(<span class="string">"Executing middlewareTwo"</span>)</span><br><span class="line">    <span class="keyword">if</span> r.URL.Path != <span class="string">"/"</span> &#123;</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    next.ServeHTTP(w, r)</span><br><span class="line">    log.Println(<span class="string">"Executing middlewareTwo again"</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">final</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">  log.Println(<span class="string">"Executing finalHandler"</span>)</span><br><span class="line">  w.Write([]<span class="keyword">byte</span>(<span class="string">"OK"</span>))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  finalHandler := http.HandlerFunc(final)</span><br><span class="line"></span><br><span class="line">  http.Handle(<span class="string">"/"</span>, middlewareOne(middlewareTwo(finalHandler)))</span><br><span class="line">  http.ListenAndServe(<span class="string">":3000"</span>, <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行这个应用程序并向 <code>http://localhost:3000</code> 发出请求。你应该会得到这样的日志输出：</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">$ go run main.go</span><br><span class="line">2014&#x2F;10&#x2F;13 20:27:36 Executing middlewareOne</span><br><span class="line">2014&#x2F;10&#x2F;13 20:27:36 Executing middlewareTwo</span><br><span class="line">2014&#x2F;10&#x2F;13 20:27:36 Executing finalHandler</span><br><span class="line">2014&#x2F;10&#x2F;13 20:27:36 Executing middlewareTwo again</span><br><span class="line">2014&#x2F;10&#x2F;13 20:27:36 Executing middlewareOne again</span><br></pre></td></tr></table></figure><p>很明显，可以看到如何通过处理程序链按照嵌套顺序传递控制权，然后再以相反的方向返回。</p><p>任何时候，我们都可以通过在中间层处理程序中返回，来停止链传递的控件。</p><p>在上面的例子中，我在中间层中包含了一个条件返回函数。通过访问 <code>http://localhost:3000/foo</code> 再次尝试，并检查日志 - 你会发现此次请求不会通过中间层进一步传递到备份链。</p><h2 id="通过一个合适的例子来了解如何？（Understood-How-About-a-Proper-Example-）"><a href="#通过一个合适的例子来了解如何？（Understood-How-About-a-Proper-Example-）" class="headerlink" title="通过一个合适的例子来了解如何？（Understood. How About a Proper Example?）"></a>通过一个合适的例子来了解如何？（Understood. How About a Proper Example?）</h2><p>好了。假设我们正在构建一个处理正文中包含 XML 请求的服务。我们想要创建一些中间层，它们 a）检查请求体是否存在，b）嗅探以确保它是 XML（格式）。如果其中任何一项检查失败，我们希望我们的中间层写入错误消息并停止将请求传递给我们的应用处理程序。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">File: main.go</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"bytes"</span></span><br><span class="line">  <span class="string">"net/http"</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">enforceXMLHandler</span><span class="params">(next http.Handler)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">    <span class="comment">// Check for a request body</span></span><br><span class="line">    <span class="keyword">if</span> r.ContentLength == <span class="number">0</span> &#123;</span><br><span class="line">      http.Error(w, http.StatusText(<span class="number">400</span>), <span class="number">400</span>)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Check its MIME type</span></span><br><span class="line">    buf := <span class="built_in">new</span>(bytes.Buffer)</span><br><span class="line">    buf.ReadFrom(r.Body)</span><br><span class="line">    <span class="keyword">if</span> http.DetectContentType(buf.Bytes()) != <span class="string">"text/xml; charset=utf-8"</span> &#123;</span><br><span class="line">      http.Error(w, http.StatusText(<span class="number">415</span>), <span class="number">415</span>)</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    next.ServeHTTP(w, r)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  finalHandler := http.HandlerFunc(final)</span><br><span class="line"></span><br><span class="line">  http.Handle(<span class="string">"/"</span>, enforceXMLHandler(finalHandler))</span><br><span class="line">  http.ListenAndServe(<span class="string">":3000"</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">final</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">  w.Write([]<span class="keyword">byte</span>(<span class="string">"OK"</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这看起来不错。我们通过创建一个简单的 XML 文件来测试它：</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></pre></td><td class="code"><pre><span class="line">$ cat &gt; books.xml</span><br><span class="line">&lt;?xml version&#x3D;&quot;1.0&quot;?&gt;</span><br><span class="line">&lt;books&gt;</span><br><span class="line">  &lt;book&gt;</span><br><span class="line">    &lt;author&gt;H. G. Wells&lt;&#x2F;author&gt;</span><br><span class="line">    &lt;title&gt;The Time Machine&lt;&#x2F;title&gt;</span><br><span class="line">    &lt;price&gt;8.50&lt;&#x2F;price&gt;</span><br><span class="line">  &lt;&#x2F;book&gt;</span><br><span class="line">&lt;&#x2F;books&gt;</span><br></pre></td></tr></table></figure><p>并使用 curl 命令发出一些请求：</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><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">$ curl -i localhost:3000</span><br><span class="line">HTTP&#x2F;1.1 400 Bad Request</span><br><span class="line">Content-Type: text&#x2F;plain; charset&#x3D;utf-8</span><br><span class="line">Content-Length: 12</span><br><span class="line"></span><br><span class="line">Bad Request</span><br><span class="line">$ curl -i -d &quot;This is not XML&quot; localhost:3000</span><br><span class="line">HTTP&#x2F;1.1 415 Unsupported Media Type</span><br><span class="line">Content-Type: text&#x2F;plain; charset&#x3D;utf-8</span><br><span class="line">Content-Length: 23</span><br><span class="line"></span><br><span class="line">Unsupported Media Type</span><br><span class="line">$ curl -i -d @books.xml localhost:3000</span><br><span class="line">HTTP&#x2F;1.1 200 OK</span><br><span class="line">Date: Fri, 17 Oct 2014 13:42:10 GMT</span><br><span class="line">Content-Length: 2</span><br><span class="line">Content-Type: text&#x2F;plain; charset&#x3D;utf-8</span><br><span class="line"></span><br><span class="line">OK</span><br></pre></td></tr></table></figure><h2 id="使用第三方中间层（Using-Third-Party-Middleware）"><a href="#使用第三方中间层（Using-Third-Party-Middleware）" class="headerlink" title="使用第三方中间层（Using Third-Party Middleware）"></a>使用第三方中间层（Using Third-Party Middleware）</h2><p>基本上你想直接使用第三方软件包而不是自己写中间层。我们将在这里看到一对（第三方软件包）：<a href="http://elithrar.github.io/article/httpauth-basic-auth-for-go/" target="_blank" rel="noopener">goji/httpauth</a> 和 Gorilla 的 <a href="http://www.gorillatoolkit.org/pkg/handlers#LoggingHandler" target="_blank" rel="noopener">LoggingHandler</a>。</p><p>goji/httpauth 包提供了 HTTP 基本的认证功能。它有一个 <a href="https://godoc.org/github.com/goji/httpauth#SimpleBasicAuth" target="_blank" rel="noopener">SimpleBasicAuth</a> helper，它返回一个带有签名的 <code>func (http.Handler) http.Handler</code> 函数。这意味着我们可以像我们定制的中间层一样（的方式）使用它。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ go get github.com&#x2F;goji&#x2F;httpauth</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">File: main.go</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></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">"github.com/goji/httpauth"</span></span><br><span class="line">  <span class="string">"net/http"</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">  finalHandler := http.HandlerFunc(final)</span><br><span class="line">  authHandler := httpauth.SimpleBasicAuth(<span class="string">"username"</span>, <span class="string">"password"</span>)</span><br><span class="line"></span><br><span class="line">  http.Handle(<span class="string">"/"</span>, authHandler(finalHandler))</span><br><span class="line">  http.ListenAndServe(<span class="string">":3000"</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">final</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">  w.Write([]<span class="keyword">byte</span>(<span class="string">"OK"</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></pre></td><td class="code"><pre><span class="line">$ curl -i username:password@localhost:3000</span><br><span class="line">HTTP&#x2F;1.1 200 OK</span><br><span class="line">Content-Length: 2</span><br><span class="line">Content-Type: text&#x2F;plain; charset&#x3D;utf-8</span><br><span class="line"></span><br><span class="line">OK</span><br><span class="line">$ curl -i username:wrongpassword@localhost:3000</span><br><span class="line">HTTP&#x2F;1.1 401 Unauthorized</span><br><span class="line">Content-Type: text&#x2F;plain; charset&#x3D;utf-8</span><br><span class="line">Www-Authenticate: Basic realm&#x3D;&quot;&quot;Restricted&quot;&quot;</span><br><span class="line">Content-Length: 13</span><br><span class="line"></span><br><span class="line">Unauthorized</span><br></pre></td></tr></table></figure><p>Gorilla 的 LoggingHandler - 它记录了 <a href="http://httpd.apache.org/docs/1.3/logs.html#common" target="_blank" rel="noopener">Apache 风格的日志</a> - 有点不一样。</p><p>它使用签名 <code>func(out io.Writer, h http.Handler) http.Handler</code>，所以它不仅需要下一个处理程序，还需要将日志写入的 io.Writer。</p><p>以下是一个简单的例子，我们将日志写入 <code>server.log</code> 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/gorilla/handlers</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">File: main.go</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></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">"github.com/gorilla/handlers"</span></span><br><span class="line">  <span class="string">"net/http"</span></span><br><span class="line">  <span class="string">"os"</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">  finalHandler := http.HandlerFunc(final)</span><br><span class="line"></span><br><span class="line">  logFile, err := os.OpenFile(<span class="string">"server.log"</span>, os.O_WRONLY|os.O_CREATE|os.O_APPEND, <span class="number">0666</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">  http.Handle(<span class="string">"/"</span>, handlers.LoggingHandler(logFile, finalHandler))</span><br><span class="line">  http.ListenAndServe(<span class="string">":3000"</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">final</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">  w.Write([]<span class="keyword">byte</span>(<span class="string">"OK"</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这种小例子中，我们的代码是非常清晰的。但是如果我们想将 LoggingHandler 用作更大的中间层链的一部分，会发生什么？我们可以很容易地得到一个看起来像这样的声明…</p><p><code>http.Handle(&quot;/&quot;, handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))</code></p><p>… 那让我的头疼！</p><p>一种已经知道的方法是通过创建一个构造函数（让我们称之为 myLoggingHandler）和签名 <code>func (http.Handler) http.Handler</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="function"><span class="keyword">func</span> <span class="title">myLoggingHandler</span><span class="params">(h http.Handler)</span> <span class="title">http</span>.<span class="title">Handler</span></span> &#123;</span><br><span class="line">  logFile, err := os.OpenFile(<span class="string">"server.log"</span>, os.O_WRONLY|os.O_CREATE|os.O_APPEND, <span class="number">0666</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 class="keyword">return</span> handlers.LoggingHandler(logFile, h)</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">  finalHandler := http.HandlerFunc(final)</span><br><span class="line"></span><br><span class="line">  http.Handle(<span class="string">"/"</span>, myLoggingHandler(finalHandler))</span><br><span class="line">  http.ListenAndServe(<span class="string">":3000"</span>, <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果你运行这个应用程序并发送一些请求，你的 server.log 文件应该是这样的：</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></pre></td><td class="code"><pre><span class="line">$ cat server.log</span><br><span class="line">127.0.0.1 - - [21&#x2F;Oct&#x2F;2014:18:56:43 +0100] &quot;GET &#x2F; HTTP&#x2F;1.1&quot; 200 2</span><br><span class="line">127.0.0.1 - - [21&#x2F;Oct&#x2F;2014:18:56:36 +0100] &quot;POST &#x2F; HTTP&#x2F;1.1&quot; 200 2</span><br><span class="line">127.0.0.1 - - [21&#x2F;Oct&#x2F;2014:18:56:43 +0100] &quot;PUT &#x2F; HTTP&#x2F;1.1&quot; 200 2</span><br></pre></td></tr></table></figure><p>如果你有兴趣，可以参考这篇文章中的三个中间层处理程序。</p><p>附注：请注意，Gorilla LoggingHandler 正在记录日志中的响应状态（<code>200</code>）和响应长度（<code>2</code>）。这很有趣。上游的日志记录中间层是如何知道我们的应用处理程序编写的响应的？</p><p>它通过定义自己的 <code>responseLogger</code> 类来包装 <code>http.ResponseWriter</code>，并创建自定义的 <code>reponseLogger.Write()</code> 和 <code>reponseLogger.WriteHeader()</code> 方法。这些方法不仅可以编写响应，还可以存储大小和状态供以后检查。Gorilla 的 LoggingHandler 将 <code>reponseLogger</code> 传递给链中的下一个处理程序，而不是普通的 <code>http.ResponseWriter</code>。</p><h2 id="附加工具（Additional-Tools）"><a href="#附加工具（Additional-Tools）" class="headerlink" title="附加工具（Additional Tools）"></a>附加工具（Additional Tools）</h2><p><a href="https://github.com/justinas/alice" target="_blank" rel="noopener">由 Justinas Stankevičius 编写的 Alice</a> 是一个非常聪明并且轻量级的包，它为连接中间层处理程序提供了一些语法糖。在最基础的方面，Alice 允许你重写这个：</p><p><code>http.Handle(&quot;/&quot;, myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))</code></p><p>为这个：</p><p><code>http.Handle(&quot;/&quot;, alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))</code></p><p>至少在我看来，这些代码一眼就能看清楚这一点。但是，Alice 的真正好处是它可以让你指定一个处理程序链并将其重复用于多个路由。像这样：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><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">stdChain := alice.New(myLoggingHandler, authHandler, enforceXMLHandler)</span><br><span class="line"></span><br><span class="line">http.Handle(<span class="string">"/foo"</span>, stdChain.Then(fooHandler))</span><br><span class="line">http.Handle(<span class="string">"/bar"</span>, stdChain.Then(barHandler))</span><br></pre></td></tr></table></figure><hr><p>via: <a href="http://www.alexedwards.net/blog/making-and-using-middleware" target="_blank" rel="noopener">http://www.alexedwards.net/blog/making-and-using-middleware</a></p><p>作者：<a href="https://katcipis.github.io/" target="_blank" rel="noopener">TIAGO KATCIPIS</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/校对者ID" target="_blank" rel="noopener">校对者ID</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在构建 Web 应用时，可能需要为许多（甚至全部）HTTP 请求运行一些公共的函数。你可能需要对每个请求进行记录，对每个响应进行压缩，或者在执行一些重要的处理之前检查一下缓存。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Golang 数据库 mocks</title>
    <link href="http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91Golang-%E6%95%B0%E6%8D%AE%E5%BA%93-mocks/"/>
    <id>http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91Golang-%E6%95%B0%E6%8D%AE%E5%BA%93-mocks/</id>
    <published>2018-04-15T13:56:31.000Z</published>
    <updated>2020-05-02T14:10:23.385Z</updated>
    
    <content type="html"><![CDATA[<p>我们<a href="https://heupr.io/" target="_blank" rel="noopener">当前项目的核心</a>是一个 <a href="https://www.memsql.com/" target="_blank" rel="noopener">MemSQL</a> 数据库，它是我们核心的数据管道；这是一个非常酷的技术，它的速度非常快，我们实在是太喜欢它了。但是，测试跟它相关的代码却有点困难，这个问题通过试验或者当遇到错误（主要是遇到错误）时，很快就可以发现。由于 Go 标准包已通过全面的测试，我只需要确保调用和依赖他们的代码在生产中也能够正常运行就好。</p><a id="more"></a><p>测试我们项目的数据库代码，我通过两个步骤完成。</p><p>通过 database/sql 包，我们有了 sql.DB 结构，它代表一系列mocks的连接，以及包含一系列与这些连接进行交互的方法。在我们的代码库中，我们使用了其中的两个（以及一个返回打开的数据库的函数）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Open</span><span class="params">(driverName, dataSourceName <span class="keyword">string</span>)</span> <span class="params">(*DB, error)</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(db *DB)</span> <span class="title">Close</span><span class="params">()</span> <span class="title">error</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(db *DB)</span> <span class="title">Query</span><span class="params">(query <span class="keyword">string</span>, args ...<span class="keyword">interface</span>&#123;&#125;)</span> <span class="params">(*Rows, error)</span></span></span><br></pre></td></tr></table></figure><p>接着，我像下面这样创建了一个名为 sqlDB 的接口：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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> sqlDB <span class="keyword">interface</span> &#123;</span><br><span class="line">    Open(driverName, dataSourceName <span class="keyword">string</span>) (*sql.DB, error)</span><br><span class="line">    Close() error</span><br><span class="line">    Query(query <span class="keyword">string</span>, args ...<span class="keyword">interface</span>&#123;&#125;) (*sql.Rows, error)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个 “曲线救国” 的想法，我定义了一个由 sql.DB 结构方法实现的接口。有了这个接口，再加上单元测试可以直接使用测试对象，立马就可以测试围绕这些函数/方法的调用代码。请注意，由于在返回值中依赖了诸如 sql.Rows 之类的，导致需要一些额外的配置，但它确实让需要测试的范围更接近于实际的 database/sql 包（即，需要测试的东西更加少）。</p><p>接着，我构建第二个步骤的测试。定义了一个 dataAccess 接口，其中包含返回，将供后面处理的，准备好的对象的特定方法。这些函数将调用上面的 sqlDB 接口定义的方法，（下面的）这些方法从我们的代码库和单元测试中进一步封装了 database/sql 包：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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> dataAccess <span class="keyword">interface</span> &#123;</span><br><span class="line">    readIntegrations(query <span class="keyword">string</span>) (<span class="keyword">map</span>[<span class="keyword">int64</span>]*integration, error)</span><br><span class="line">    readSettings(query <span class="keyword">string</span>) (<span class="keyword">map</span>[<span class="keyword">int64</span>]*settings, error)</span><br><span class="line">    readEvents(query <span class="keyword">string</span>) (<span class="keyword">map</span>[<span class="keyword">int64</span>][]*preprocess.Container, error)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再一次使用接口，可以让我们在测试中替换更多的生产代码，并让我们在调用这些接口代码时，可以关注（更多）不同的测试用例和场景。</p><p>简单地说：</p><ul><li>sqlDB 接口可以让我们立即测试与数据库对象交互的相关代码（而不需要真的启动一个数据库并注入各种表） - （例如）测试处理数据库查询之类的东西</li><li>dataAccess 接口允许我们对依赖它的值进行单元测试，例如上面返回的 map 结果 - 后面的想法也都是这样</li></ul><p>对（依赖于）数据库相关的代码进行单元测试有不同的方法，而这就是我在 Go 项目中采用的方法 - 我非常乐意提供（我的）建议，或者方案，以及快乐地进行单元测试的方法！</p><hr><p>via: <a href="https://dev.to/forstmeier/golang-database-mocks-1hm9" target="_blank" rel="noopener">https://dev.to/forstmeier/golang-database-mocks-1hm9</a></p><p>作者：<a href="https://dev.to/forstmeier" target="_blank" rel="noopener">John Forstmeier</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/rxcai" target="_blank" rel="noopener">rxcai</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;我们&lt;a href=&quot;https://heupr.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;当前项目的核心&lt;/a&gt;是一个 &lt;a href=&quot;https://www.memsql.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MemSQL&lt;/a&gt; 数据库，它是我们核心的数据管道；这是一个非常酷的技术，它的速度非常快，我们实在是太喜欢它了。但是，测试跟它相关的代码却有点困难，这个问题通过试验或者当遇到错误（主要是遇到错误）时，很快就可以发现。由于 Go 标准包已通过全面的测试，我只需要确保调用和依赖他们的代码在生产中也能够正常运行就好。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】接口不是值类型</title>
    <link href="http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91%E6%8E%A5%E5%8F%A3%E4%B8%8D%E6%98%AF%E5%80%BC%E7%B1%BB%E5%9E%8B/"/>
    <id>http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91%E6%8E%A5%E5%8F%A3%E4%B8%8D%E6%98%AF%E5%80%BC%E7%B1%BB%E5%9E%8B/</id>
    <published>2018-04-15T13:54:32.000Z</published>
    <updated>2020-05-02T14:08:44.189Z</updated>
    
    <content type="html"><![CDATA[<h2 id="介绍（Introduction）"><a href="#介绍（Introduction）" class="headerlink" title="介绍（Introduction）"></a>介绍（Introduction）</h2><p>最近，在 Slack 上我看过大量关于接口的问题。大多数时候，答案都很有技术性，并都关注了实现的细节。实现（细节）对于调试很有帮助，但实现对设计却毫无帮助。当用接口来设计代码时，行为才是主要需要关注的。</p><a id="more"></a><p>在这篇博文中，我希望提供一个不同的思考方式，关于接口，和用他们进行代码设计。我想让你停止关注于实现细节，而是关注于接口和具体的数据的关系。</p><h2 id="面向数据设计（Data-Oriented-Design）"><a href="#面向数据设计（Data-Oriented-Design）" class="headerlink" title="面向数据设计（Data Oriented Design）"></a>面向数据设计（Data Oriented Design）</h2><p>我相信写 Go 代码，应该用面向数据设计的方法，而不是面向对象。我的面向数据的第一条原则是：</p><p>如果你不了解你要处理的数据，你肯定不懂你要解决的问题。</p><p>所有你要解决的问题本质上就是数据转换的问题。有一些输入，然后你产生输出。这就是程序要做的事情。每一个你写的函数都是一个小的数据转换，（它们只是）为了帮助你解决大的数据转换。</p><p>因为你要解决的问题就是数据转换的问题，你写的算法要基于具体的数据。具体数据就是你存储在内存中的物理状态，通过网络发送，写入文件并进行基本操作。<a href="https://mechanical-sympathy.blogspot.com/" target="_blank" rel="noopener">机器情绪</a>取决于具体的数据和你允许你的机器做怎样的数据转换。</p><p>对面向数据的一个大的警告是关于如何处理修改。关于面向数据，我的第二条原则是：</p><p>当数据修改时，你的问题就修改了。当你的问题修改了，那么你的算法就要跟着修改。</p><p>一旦数据修改了，你的算法就需要修改。这是保证可读性和性能的最好方式。不幸的是，我们大多数人都被教导创建更多的抽象层，来处理变化。当设计需要修改时，我认为这种方式（创建更多的抽象层）将得不偿失。</p><p>你需要的是允许你的算法保持精简，来执行需要的数据转换。当数据修改时，你需要这样一种方式，算法改变了但却不会导致整个代码库的大部分代码发生级联变化。这就是使用接口的时候。当你关注接口时，你其实想要关注的是行为。</p><h2 id="具体数据（Concrete-Data）"><a href="#具体数据（Concrete-Data）" class="headerlink" title="具体数据（Concrete Data）"></a>具体数据（Concrete Data）</h2><p>因为每种事情都跟具体的数据有关，你应该从具体的数据开始。从具体的类型开始。</p><h3 id="代码清单-1"><a href="#代码清单-1" class="headerlink" title="代码清单 1"></a>代码清单 1</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">05</span> <span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">06</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">07</span> &#125;</span><br></pre></td></tr></table></figure><p>在代码清单 1 中的第 5 行，关键字 <code>struct</code> 声明了一个名为 file 的类型。有了这个具体的类型声明，你可以创建一个这种类型的值。</p><h3 id="代码清单-2"><a href="#代码清单-2" class="headerlink" title="代码清单 2"></a>代码清单 2</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">13</span> <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="number">14</span>     <span class="keyword">var</span> f file</span><br></pre></td></tr></table></figure><p>多亏了代码清单 2 中的第 14 行的声明，现在你有一个类型为 file，存在内存中，被命名为 f 的变量，并引用了具体的数据。这个数据被变量 f 索引，而且可以被操纵。</p><p>你可以再次使用关键字 struct 来定义第二块具体数据。</p><h3 id="代码清单-3"><a href="#代码清单-3" class="headerlink" title="代码清单 3"></a>代码清单 3</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">09</span> <span class="keyword">type</span> pipe <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">10</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">11</span> &#125;</span><br></pre></td></tr></table></figure><p>在代码清单 3 中的第 09 行声明了类型为 <code>pipe</code>，并拥有一部分具体的数据。再一次，有了这个类型的声明，你可以在程序中，创建一个不同的值。</p><h3 id="代码清单-4"><a href="#代码清单-4" class="headerlink" title="代码清单 4"></a>代码清单 4</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">01</span> <span class="keyword">package</span> main</span><br><span class="line"><span class="number">02</span></span><br><span class="line"><span class="number">03</span> <span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="number">04</span></span><br><span class="line"><span class="number">05</span> <span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">06</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">07</span> &#125;</span><br><span class="line"><span class="number">08</span></span><br><span class="line"><span class="number">09</span> <span class="keyword">type</span> pipe <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">10</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">11</span> &#125;</span><br><span class="line"><span class="number">12</span></span><br><span class="line"><span class="number">13</span> <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="number">14</span>     <span class="keyword">var</span> f file</span><br><span class="line"><span class="number">15</span>     <span class="keyword">var</span> p pipe</span><br><span class="line"><span class="number">16</span></span><br><span class="line"><span class="number">17</span>     fmt.Println(f, p) </span><br><span class="line"><span class="number">18</span> &#125;</span><br></pre></td></tr></table></figure><p>现在，这个程序拥有两个清晰的具体数据定义，以及对应的一个值。在第 14 行，一个类型为 file 的值被创建，在第 15 行，一个类型为 pipe 的值被创建。为了程序完整，两个值在第 17 行都被 fmt 包展示出来。</p><h2 id="接口值不是值类型（Interfaces-Are-Valueless）"><a href="#接口值不是值类型（Interfaces-Are-Valueless）" class="headerlink" title="接口值不是值类型（Interfaces Are Valueless）"></a>接口值不是值类型（Interfaces Are Valueless）</h2><p>你已经用关键字 <code>struct</code> 定义了你程序需要的值。还有另外一个关键字可以用来定义类型。那就是关键字 <code>interface</code>。</p><h3 id="代码清单-5"><a href="#代码清单-5" class="headerlink" title="代码清单 5"></a>代码清单 5</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">05</span> <span class="keyword">type</span> reader <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="number">06</span>     read(b []<span class="keyword">byte</span>) (<span class="keyword">int</span>, error)</span><br><span class="line"><span class="number">07</span> &#125;</span><br></pre></td></tr></table></figure><p>在代码清单 5 第 05 行，声明了一个 <code>interface</code> 的类型。<code>interface</code> 类型跟 <code>struct</code> 类型相对应。 <code>interface</code> 类型只能声明一组行为的方法。这意味着 <code>interface</code> 类型没有具体的值。</p><h3 id="代码清单-6"><a href="#代码清单-6" class="headerlink" title="代码清单 6"></a>代码清单 6</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> r reader</span><br><span class="line"><span class="string">``</span><span class="string">` </span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">有趣的是你可以声明一个 `</span><span class="keyword">interface</span><span class="string">` 类型的变量，就像代码清单 6 中展示的一样。这非常有趣，因为如果在 `</span><span class="keyword">interface</span><span class="string">` 中没有具体的值，那么变量 `</span>r<span class="string">` 似乎就是毫无意义的。`</span><span class="keyword">interface</span><span class="string">` 类型定义以及创建的值是毫无价值的！</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Boom！大脑爆炸了。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">这是一个非常重要的概念。你必须明白：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">- 变量 `</span>r<span class="string">` 不代表任何东西。</span></span><br><span class="line"><span class="string">- 变量 `</span>r<span class="string">` 没有具体的值。</span></span><br><span class="line"><span class="string">- 变量 `</span>r<span class="string">` 毫无意义。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">有一个实现细节使得 r 在后台是真实存在的，但从我们的编程模型来看，它却是不存在的。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">当你认识到 `</span><span class="keyword">interface</span><span class="string">` 不是值类型，整个世界就变得清晰可以理解了。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">### 代码清单 7</span></span><br><span class="line"><span class="string">`</span><span class="string">``</span><span class="keyword">go</span></span><br><span class="line"><span class="number">37</span> <span class="function"><span class="keyword">func</span> <span class="title">retrieve</span><span class="params">(r reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="number">38</span>     data := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">100</span>)</span><br><span class="line"><span class="number">39</span></span><br><span class="line"><span class="number">40</span>     <span class="built_in">len</span>, err := r.read(data)</span><br><span class="line"><span class="number">41</span>     <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="number">42</span>         <span class="keyword">return</span> err</span><br><span class="line"><span class="number">43</span>     &#125;</span><br><span class="line"><span class="number">44</span></span><br><span class="line"><span class="number">45</span>     fmt.Println(<span class="keyword">string</span>(data[:<span class="built_in">len</span>]))</span><br><span class="line"><span class="number">46</span>     <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"><span class="number">47</span> &#125;</span><br><span class="line"><span class="string">``</span><span class="string">` </span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">在代码清单 7 定义了 `</span>retrieve<span class="string">` 函数，一个我称之为多态的函数。在我继续前，先说明一下，多态的定义是按顺序的。看下来自于 Basic 的发明人 Tom Kurtz 的定义，这个定义会让你觉得多态函数是如此的特别。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">“多态性意味着你写的一个特定的程序，它的行为会有所不同，而这取决于它所操作的数据。”</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">当我看到这个观点时，它总让我惊讶。它的简洁，却很好地说明了一点。多态性由具体的数据驱动。具有改变代码行为能力的是具体的数据。正如我以上所说的，你正在解决的问题是植根于具体的数据。面向数据设计是基于具体数据的。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">如果你不懂你正在使用的【具体】数据，你就不懂你想要解决的问题。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Tom 的观点已经清楚地表明，具体的数据才是设计实现不同行为（多态性）抽象的关键。多么聪明的观点。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">再回到代码清单 7。我将在下面重复一遍。</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">### 代码清单 7 - 复制</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">`</span><span class="string">``</span><span class="keyword">go</span></span><br><span class="line"><span class="number">37</span> <span class="function"><span class="keyword">func</span> <span class="title">retrieve</span><span class="params">(r reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="number">38</span>     data := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">100</span>)</span><br><span class="line"><span class="number">39</span></span><br><span class="line"><span class="number">40</span>     <span class="built_in">len</span>, err := r.read(data)</span><br><span class="line"><span class="number">41</span>     <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="number">42</span>         <span class="keyword">return</span> err</span><br><span class="line"><span class="number">43</span>     &#125;</span><br><span class="line"><span class="number">44</span></span><br><span class="line"><span class="number">45</span>     fmt.Println(<span class="keyword">string</span>(data[:<span class="built_in">len</span>]))</span><br><span class="line"><span class="number">46</span>     <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"><span class="number">47</span> &#125;</span><br></pre></td></tr></table></figure><p>当你读到第 37 行的 retrieve 函数声明时，函数似乎在说，传递给我一个类型为 reader 的值。但你知道这不可能，因为根本就没有一个值的类型为 reader。类型为 reader 的值压根不存在，因为 reader 是一个接口类型。我们都知道接口不是值类型。</p><p>那么函数到底想说什么？它想说的是：</p><p>我会接受任何实现了 reader 接口的具体数据（任何值或者指针）。但它必须实现 reader 接口定义的所有方法。</p><p>这就是你如何在 Go 中实现多态的方式。retrieve 函数不绑定到单个具体数据，而是绑定到任何实现 reader 接口的具体数据。</p><h2 id="给数据赋予行为（Giving-Data-Behavior）"><a href="#给数据赋予行为（Giving-Data-Behavior）" class="headerlink" title="给数据赋予行为（Giving Data Behavior）"></a>给数据赋予行为（Giving Data Behavior）</h2><p>接下来的问题是，如何给数据赋予行为？这就是方法的用处。方法提供数据的行为机制。一旦数据有了行为方法，就可以实现多态。</p><p>”多态意味着你写的一个确定的程序，但他的行为可能不同，而这依赖于它所操作的数据。“</p><p>在 Go 中，你可以写函数和方法。选择方法而不是函数的一个原因是，数据被要求要实现给定接口的方法集。</p><h3 id="代码清单-8"><a href="#代码清单-8" class="headerlink" title="代码清单 8"></a>代码清单 8</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">05</span> <span class="keyword">type</span> reader <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="number">06</span>     read(b []<span class="keyword">byte</span>) (<span class="keyword">int</span>, error)</span><br><span class="line"><span class="number">07</span> &#125;</span><br><span class="line"><span class="number">08</span></span><br><span class="line"><span class="number">09</span> <span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">10</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">11</span> &#125;</span><br><span class="line"><span class="number">12</span></span><br><span class="line"><span class="number">13</span> <span class="function"><span class="keyword">func</span> <span class="params">(file)</span> <span class="title">read</span><span class="params">(b []<span class="keyword">byte</span>)</span> <span class="params">(<span class="keyword">int</span>, error)</span></span> &#123;</span><br><span class="line"><span class="number">14</span>     s := <span class="string">"&lt;rss&gt;&lt;channel&gt;&lt;title&gt;Going Go&lt;/title&gt;&lt;/channel&gt;&lt;/rss&gt;"</span></span><br><span class="line"><span class="number">15</span>     <span class="built_in">copy</span>(b, s)</span><br><span class="line"><span class="number">16</span>     <span class="keyword">return</span> <span class="built_in">len</span>(s), <span class="literal">nil</span></span><br><span class="line"><span class="number">17</span> &#125;</span><br><span class="line"><span class="number">18</span></span><br><span class="line"><span class="number">19</span> <span class="keyword">type</span> pipe <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">20</span>     name <span class="keyword">string</span></span><br><span class="line"><span class="number">21</span> &#125;</span><br><span class="line"><span class="number">22</span></span><br><span class="line"><span class="number">23</span> <span class="function"><span class="keyword">func</span> <span class="params">(pipe)</span> <span class="title">read</span><span class="params">(b []<span class="keyword">byte</span>)</span> <span class="params">(<span class="keyword">int</span>, error)</span></span> &#123;</span><br><span class="line"><span class="number">24</span>     s := <span class="string">`&#123;name: "bill", title: "developer"&#125;`</span></span><br><span class="line"><span class="number">25</span>     <span class="built_in">copy</span>(b, s)</span><br><span class="line"><span class="number">26</span>     <span class="keyword">return</span> <span class="built_in">len</span>(s), <span class="literal">nil</span></span><br><span class="line"><span class="number">27</span> &#125;</span><br></pre></td></tr></table></figure><p>请注意：你可能注意在接收者的方法中的第 13 行和第 23 行，声明了但没有给一个变量具体的名字。这其实是惯例，如果这个方法不需要使用接收者的任何数据时就可以不给接收者一个具体的名字。</p><p>在代码清单 8，在第 13 行，为类型 file 定义了一个方法，在第23 行，为 pipe 类型定义了一个方法。现在，每种类型都定义了一个名为 read 的方法，它已经实现了 reader 定义的所有方法。由于有了这些方法的定义，接下来我们可以说：</p><p>“类型 file 和 pipe 现在已经实现了 reader 接口。”</p><p>我在那段话中所说的每一句都很重要。如果你有看我之前关于值和指针语义的博客文章，那么你应该知道数据展现的行为由你正在使用的语义决定的。在这篇文章中我不会再讨论这些。这里有一个链接。</p><p><a href="https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html</a></p><p>一旦这些值，值和指针，实现了这些方法，它们就可以传递给多态函数 retrieve。</p><h3 id="代码清单-9"><a href="#代码清单-9" class="headerlink" title="代码清单 9"></a>代码清单 9</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> reader <span class="keyword">interface</span> &#123;</span><br><span class="line">   read(b []<span class="keyword">byte</span>) (<span class="keyword">int</span>, error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> file <span class="keyword">struct</span> &#123;</span><br><span class="line">   name <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(file)</span> <span class="title">read</span><span class="params">(b []<span class="keyword">byte</span>)</span> <span class="params">(<span class="keyword">int</span>, error)</span></span> &#123;</span><br><span class="line">   s := <span class="string">"&lt;rss&gt;&lt;channel&gt;&lt;title&gt;Going Go&lt;/title&gt;&lt;/channel&gt;&lt;/rss&gt;"</span></span><br><span class="line">   <span class="built_in">copy</span>(b, s)</span><br><span class="line">   <span class="keyword">return</span> <span class="built_in">len</span>(s), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> pipe <span class="keyword">struct</span> &#123;</span><br><span class="line">   name <span class="keyword">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(pipe)</span> <span class="title">read</span><span class="params">(b []<span class="keyword">byte</span>)</span> <span class="params">(<span class="keyword">int</span>, error)</span></span> &#123;</span><br><span class="line">   s := <span class="string">`&#123;name: "bill", title: "developer"&#125;`</span></span><br><span class="line">   <span class="built_in">copy</span>(b, s)</span><br><span class="line">   <span class="keyword">return</span> <span class="built_in">len</span>(s), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">   f := file&#123;<span class="string">"data.json"</span>&#125;</span><br><span class="line">   p := pipe&#123;<span class="string">"cfg_service"</span>&#125;</span><br><span class="line"></span><br><span class="line">   retrieve(f)</span><br><span class="line">   retrieve(p)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">retrieve</span><span class="params">(r reader)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">   data := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">100</span>)</span><br><span class="line"></span><br><span class="line">   <span class="built_in">len</span>, err := r.read(data)</span><br><span class="line">   <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">       <span class="keyword">return</span> err</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   fmt.Println(<span class="keyword">string</span>(data[:<span class="built_in">len</span>]))</span><br><span class="line">   <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码清单 9 在 Go 中提供了一个完整的多态实例，并很好的说明了接口不是值类型这个观点。retrieve 函数可以接受任何实现了 reader 接口的数据，任何值或者指针。这正是你在第 33 行和第 34 行的函数调用中可以看到的情况。</p><p>现在，你可以看到 Go 中如何实现高级别的解耦，而且这种解耦还是非常地确切。你现在完全明白了数据的行为将传递为函数的行为。阅读代码时，这不再是陌生或无法理解的了。</p><p>当你接受接口不是值类型的时候，这一切就都可以说得通。这个函数不是要求 reader 值，因为 reader 值根本不存在。该函数要求的是实现 reader 定义的方法的具体数据。</p><h2 id="接口值的分配（Interface-Value-Assignments）"><a href="#接口值的分配（Interface-Value-Assignments）" class="headerlink" title="接口值的分配（Interface Value Assignments）"></a>接口值的分配（Interface Value Assignments）</h2><p>接口不是值类型的观点可以延伸到接口值的分配。看下这些接口类型。</p><h3 id="代码清单-10"><a href="#代码清单-10" class="headerlink" title="代码清单 10"></a>代码清单 10</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">05</span> <span class="keyword">type</span> Reader <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="number">06</span>     Read()</span><br><span class="line"><span class="number">07</span> &#125;</span><br><span class="line"><span class="number">08</span></span><br><span class="line"><span class="number">09</span> <span class="keyword">type</span> Writer <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="number">10</span>     Write()</span><br><span class="line"><span class="number">11</span> &#125;</span><br><span class="line"><span class="number">12</span></span><br><span class="line"><span class="number">13</span> <span class="keyword">type</span> ReadWriter <span class="keyword">interface</span> &#123;</span><br><span class="line"><span class="number">14</span>     Reader</span><br><span class="line"><span class="number">15</span>     Writer</span><br><span class="line"><span class="number">16</span> &#125;</span><br></pre></td></tr></table></figure><p>有了这些接口声明，你可以实现一个实现了所有这三个接口的具体类型。</p><h3 id="代码清单-11"><a href="#代码清单-11" class="headerlink" title="代码清单 11"></a>代码清单 11</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">18</span> <span class="keyword">type</span> system <span class="keyword">struct</span>&#123;</span><br><span class="line"><span class="number">19</span>     Host <span class="keyword">string</span></span><br><span class="line"><span class="number">20</span> &#125;</span><br><span class="line"><span class="number">21</span></span><br><span class="line"><span class="number">22</span> <span class="function"><span class="keyword">func</span> <span class="params">(*system)</span> <span class="title">Read</span><span class="params">()</span></span>  &#123; <span class="comment">/* ... */</span> &#125;</span><br><span class="line"><span class="number">23</span> <span class="function"><span class="keyword">func</span> <span class="params">(*system)</span> <span class="title">Write</span><span class="params">()</span></span> &#123; <span class="comment">/* ... */</span> &#125;</span><br></pre></td></tr></table></figure><p>下面，你可以再一次确认，接口为何不是值类型。</p><h3 id="代码清单-12"><a href="#代码清单-12" class="headerlink" title="代码清单 12"></a>代码清单 12</h3><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">25 func main() &#123;</span><br><span class="line">26     var rw ReadWriter &#x3D; &amp;system&#123;&quot;127.0.0.1&quot;&#125;</span><br><span class="line">27     var r Reader &#x3D; rw</span><br><span class="line">28     fmt.Println(rw, r)</span><br><span class="line">29 &#125;</span><br><span class="line"></span><br><span class="line">&#x2F;&#x2F; OUTPUT</span><br><span class="line">&amp;&#123;127.0.0.1&#125; &amp;&#123;127.0.0.1&#125;</span><br></pre></td></tr></table></figure><p>代码清单 12 的第 26 行，声明了一个类型为 ReadWriter，名字为 rw 的变量，并分配了一段具体的数据。具体数据是一个指向 system 的指针。然后在第 27 行中定义了类型为 Reader，名称为 r 的变量。有一个赋值操作跟这个声明相关。接口类型为 ReadWriter 的 rw 变量分配给了接口类型为 Reader 的新变量 r。</p><p>这应该会导致我们暂停一秒，因为变量 rw 和 r 的类型不同。我们知道在 Go 中两个不同名称的类型之间不会进行隐式地转换。但这还跟我们这种情况不一样。因为这些变量不是具体的值类型，它们是接口类型。</p><p>如果我们回到接口不是值类型的理解上，那么 rw 和 r 就都不是具体的值。因此，代码不能将接口值分配给对方。它唯一可以分配的是存储在接口值中的具体数据。幸亏有接口的类型声明，编译器可以验证一个接口内部的具体数据是否也满足另外的接口。</p><p>最后，我们只能处理具体的数据。处理接口值时，我们仍然只能处理存储在其中的具体数据。当你将接口值传递给 fmt 包进行显示时，请记住具体的数据就是显示的内容。再一次强调，他是唯一真实的东西。</p><h2 id="结论（Conclusion）"><a href="#结论（Conclusion）" class="headerlink" title="结论（Conclusion）"></a>结论（Conclusion）</h2><p>我希望这篇文章能给你提供一种思考接口以及如何设计代码的不同方式的参考。我相信，一旦你摆脱了实现细节，并专注于接口与具体数据之间的关系，那么事情就会变得更加合理。面向数据的设计是编写更好的算法的方式，但要求关注对行为的解耦。接口允许通过调用具体数据的方法来达到行为的解耦。</p><hr><p>via: <a href="https://www.ardanlabs.com/blog/2018/03/interface-values-are-valueless.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2018/03/interface-values-are-valueless.html</a></p><p>作者：<a href="https://github.com/ardanlabs/gotraining" target="_blank" rel="noopener">William Kennedy</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;介绍（Introduction）&quot;&gt;&lt;a href=&quot;#介绍（Introduction）&quot; class=&quot;headerlink&quot; title=&quot;介绍（Introduction）&quot;&gt;&lt;/a&gt;介绍（Introduction）&lt;/h2&gt;&lt;p&gt;最近，在 Slack 上我看过大量关于接口的问题。大多数时候，答案都很有技术性，并都关注了实现的细节。实现（细节）对于调试很有帮助，但实现对设计却毫无帮助。当用接口来设计代码时，行为才是主要需要关注的。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】小函数可能是有害的</title>
    <link href="http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91%E5%B0%8F%E5%87%BD%E6%95%B0%E5%8F%AF%E8%83%BD%E6%98%AF%E6%9C%89%E5%AE%B3%E7%9A%84/"/>
    <id>http://yoursite.com/2018/04/15/%E3%80%90%E8%AF%91%E3%80%91%E5%B0%8F%E5%87%BD%E6%95%B0%E5%8F%AF%E8%83%BD%E6%98%AF%E6%9C%89%E5%AE%B3%E7%9A%84/</id>
    <published>2018-04-15T13:02:03.000Z</published>
    <updated>2020-05-02T14:08:28.298Z</updated>
    
    <content type="html"><![CDATA[<p>在这篇博文中，我的目标是：</p><ul><li>揭示小函数的一些似是而非的优点</li><li>解释为什么我个人认为有点不像建议说的那么好</li><li>解释为什么小函数有时是适得其反</li><li>解释一下我认为小函数在 mock 中真正有用的地方<a id="more"></a></li></ul><p>通常，编程建议总是说使用更优雅和有益的小函数。《Code Clean》被普遍认为是一本编程圣经，它有一章专门讲述函数，文章的开始就是介绍一个非常长，令人头疼的函数。该书认为该函数的最大问题是长度过长，并指出：</p><p>它（函数）不仅长度太长，而且有多处重复的代码，奇怪的字符串，许多奇怪和不明了的数据类型和 API。三分钟的学习后，你能了解函数的功能吗？也许不能。那里有太多的抽象层次。奇怪的字符串，奇怪的函数调用混合在双重嵌套，并由标志位控制的 if 语句中。</p><p>本章简单地思考了什么样的特性会使代码 “更容易阅读和理解” 和 “允许任何一个读者都能直观地认识他们遇到的程序”，然后才说为了达到这个目的，必须将函数设置得更小一些。</p><p>函数的第一条原则是必须小。函数的第二条原则是它必须更小。</p><p>函数应该很小的观点几乎被认为是权威看法，不容质疑。在代码审查，twitter 上，会议上，关于编程的书籍和播客中，关于代码重构的最佳实践的文章中，等等。这个想法几天前以这种推文的形式再次进入我的时间线：</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-1.jpg" alt=""></p><p>Fowler 在他的推文中，链接了他关于函数长度的文章，并继续指出：</p><p>如果你不得不花费精力查看这一段代码来确定它在做什么，那么你应该把它提取到一个函数中，并以它的功能命名该函数。</p><p>一旦我接受了这个原则，我就养成了写一些非常小的函数的习惯 - 通常只有几行 <a href="https://martinfowler.com/bliki/FunctionLength.html#footnote-nested" target="_blank" rel="noopener">2</a>。任何超过半打行数的函数都会让我觉得不舒服，对我而言，只有一行代码的函数也并不罕见 <a href="https://martinfowler.com/bliki/FunctionLength.html#footnote-mine" target="_blank" rel="noopener">3</a>。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-2.jpg" alt=""></p><p>有些人很迷恋小函数，所以对任何可能看起来很复杂的逻辑抽象成一个单独的函数的想法向来都是推崇备至。</p><p>我一直在研究人们继承过来的代码库，他们将这种观点内化到完全扭曲的地步，以至于最终走向了不可挽回的地步，完全违背了这个观点的最初意愿。在这篇文章中，我希望解释一下为什么一些经常被吹捧的好处并不总是按照人们希望的方式发展，有的时候，一些观点的应用会变得适得其反。</p><h2 id="小函数的好处（Supposed-benefits-of-smaller-functions）"><a href="#小函数的好处（Supposed-benefits-of-smaller-functions）" class="headerlink" title="小函数的好处（Supposed benefits of smaller functions）"></a>小函数的好处（Supposed benefits of smaller functions）</h2><p>通常会列出一些理由来证明小函数背后的优点。</p><h3 id="只做一件事（Do-one-thing）"><a href="#只做一件事（Do-one-thing）" class="headerlink" title="只做一件事（Do one thing）"></a>只做一件事（Do one thing）</h3><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-3.jpg" alt=""></p><p>这个想法很简单 - 一个函数应该只做一件事，并做好。从表面上看，这似乎是一个非常好的想法，跟 Unix 哲学不谋而合。</p><p>当这个 ”一件事“ 需要被定义的时候，描述就变得模糊了。”一件事“ 可以是从简单的返回语句到条件表达式，通过网络调用的数学计算（等等）。正常情况下，许多时候，这个 ”一件事“ 意味着对某些（通常是业务）逻辑的单级抽象。</p><p>例如，在 Web 应用程序中，像 “创建用户” 这样的 CURD 操作可能是 “一件事”。通常，创建用户至少需要在数据库中创建记录（并处理任何伴随的可能错误）。此外，用户注册后可能还需要向他们发送欢迎电子邮件。另外，人们也可能希望可以自定义一个事件，像 kafka 这样的消息中间件，可将此事件发送给其他各个系统。</p><p>因此，“单一抽象层次” 不仅仅是一个层次。我所看到的是，那些完全理解函数应该做 “一件事” 的想法的程序员往往很难抵制将递归应用于他们编写的每个函数和方法中。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-4.jpg" alt=""></p><p>因此，我们现在不再是为了可以被理解（和测试）而抽象成一个合理的单元，而是将更小的单元划分出来，以描述 ”一件事“ 的每个组成部分，直到它完全模块化，完全 DRY（Don’t repeat yourself）。</p><h2 id="DRY-的谬论"><a href="#DRY-的谬论" class="headerlink" title="DRY 的谬论"></a>DRY 的谬论</h2><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-5.jpg" alt=""></p><p>DRY 和尽可能小的函数的倾向并不一定是同一件事，但我已经看到后者很多时候会让目标变成前者。DRY 在我看来已经是一个很好的指导原则，但实用和理性在教条地坚持下牺牲了，特别是那些信服 Rails 的程序员。</p><p>Python 的核心开发人员 Raymond Hettinger 发表了一篇名为 Beyond PEP8 的精彩演讲：<a href="https://www.youtube.com/watch?v=wf-BqAjZb8M" target="_blank" rel="noopener">那是美妙而易懂的最佳实践</a>。这是一个必须关注的话题，不仅适用于 Python 程序员，也适用于任何对编程感兴趣或以开发程序为生的人，因为它非常清楚地解释了教条式遵守 PEP8 的谬误，这是真正的 Python 风格指南，它介绍了很多底层实现。在 PEP8 上的谈话焦点并没有比可以应用的见解重要，（而且）其中许多情况是语言描述不了的。</p><p>即使你没有看完整个演讲，你也应该看下这个讲话的开头一分钟，这个演讲与 DRY 的警鸣做了令人惊讶的类比。程序员坚持要尽可能多地精简代码，会让他们只关注局部，忽略掉整体。</p><p>我对于 DRY 的主要问题在于它强制抽象成为抽象 - 嵌套和太早的抽象。由于不可能完美地抽象，所以我们只能尽可能地做到足够好的抽象。“足够好” 的定义很难，并且取决于很多因素。</p><p>在下图中，“抽象” 一词可以与 “函数” 互换使用。例如，假设我们要设计抽象层 A，我们可能需要考虑以下几点：</p><p><img src="https://cdn-images-1.medium.com/max/1600/1*Mh46Hv7CEkfVc_SKlA0d1w.png" alt=""></p><ul><li>支撑抽象概念 A 的假设性质以及它们可能持有的水平的可能性（以及可能持续多长时间）</li><li>抽象层 A（抽象层 X 和抽象层 Y）以及建立在抽象层 A（抽象层 Z）之上的任何抽象层的抽象层在其实现和设计中倾向于保持一致性，灵活性，可扩展性和正确性。</li><li>未来抽象（抽象层 M）的需求和期望可能建立在抽象 A 之上，以及可能需要在 A（抽象层 N）之下支持的任何抽象</li></ul><p>我们开发的抽象层 A 不可避免地会在未来不断被重新评估，并且很可能会部分甚至完全失效。一个能够支撑我们需要的，不可避免的修改的最重要的特征就是设计我们的抽象，使之变得更灵活。</p><p>尽可能最大限度地优化代码意味着，将来需要适应修改时，（这将）剥夺了我们自己的灵活性。我们优化时也要做到让自己有足够的余地来适应不可避免的变化，迟早会有这样的要求，而不是马上为了完美的契合而进行优化。</p><p>最好的抽象是优化得足够好，但不完美的抽象。这是一个函数，而不是一个错误。理解抽象的这种非常突出的性质是设计好程序的关键。</p><p>Alex Martelli 是鸭子理论和蟒蛇派的名人，他著名的演讲 “抽象塔” 中的幻灯片非常值得一读。</p><p><img src="https://cdn-images-1.medium.com/max/1600/1*fvfBJ21qOdt3XGAFHa0oOg.png" alt=""></p><p><img src="https://cdn-images-1.medium.com/max/1600/1*i5vRl8dA8docZutvy-LgYA.png" alt=""></p><p>Rubyist Sandi Metz 有一场名为 All The Little Things 的著名演讲，她认为 “重复比错误的抽象代价更低”，因此 “倾向于重复的抽象”。</p><p>在我看来，抽象概念不可能是完全 “正确的” 或者 “错误的”，因为划分 “正确” 与 “错误” 的界限本来就很模糊。实际上，我们精心设计的 “完美” 抽象，只是一个业务的要求或者被委托的一个错误的缺陷报告。</p><p>我认为这有助于将抽象视为图谱，如我们在本文前面看到的图表一样。该图谱的一端优化精度，我们代码的每个方面，最后都要求要精确。这当然有其好处，但是因为努力寻求完美的对齐方式，所以并不适合好的抽象。该图谱的另外一端优化，带来了不精确性和缺少边界。虽然这确实允许最大的灵活性，但我发现这种极端的倾向将导致其他的缺点。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-6.jpg" alt=""></p><p>跟其他大多数事情一样，“理想模型” 处于这两者之间。没有一种娱乐能取悦所有人。这个 “理想模型” 也取决于许多因素 - 工程和社会关系 - 并且，良好的工程是能够确定这个 “理想模型” 在不同环境中所处的位置，并能不断地重新评估并校准这个模型。</p><h3 id="给抽象命名（The-name-of-the-game）"><a href="#给抽象命名（The-name-of-the-game）" class="headerlink" title="给抽象命名（The name of the game）"></a>给抽象命名（The name of the game）</h3><p>说到抽象，一旦确定了抽象什么以及如何抽象，就需要给它一个名称。</p><p>给事物命名向来都很难。</p><p>这种方式（给抽象命名）普遍认为是编程过程中，使代码能活得更长的有效办法，更具描述性的名称是一件好事，甚至有人主张用带有注释的名称代替代码中的注释。他们的想法是，一个名称越具描述性，意味着封装得越好。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-7.jpg" alt=""></p><p>这个观点在 Java 的世界里普遍存在，（ Java 程序中）冗长的名称非常常见，但我从来没有发现这些冗长的名称使代码更加容易阅读。例如，可能 4-5 行的代码中就隐藏一个名字非常长的函数。当我正在阅读代码时，突然一个非常长的单词出现，会让我停下来，因为我得试图处理这个函数名称中的所有不同的音节，尝试将它融入到我已创建的心智模型中，然后决定，是否通过跳转到它定义的地方，来看它的具体实现。</p><p>然而，“小函数” 的问题在于追寻小函数的过程中导致了更多的小函数，所有这些函数都倾向于在记录自己和避免讨论的过程中给出了非常冗长的名称。</p><p>结果，处理描述详细的函数（和变量）的名称带来了认知的开销，以及将它们映射到我迄今为止构建的心智模型中，以确定哪些函数需要深入探究，哪些函数可以剔除，并将这些拼图拼在一起以揭开程序的面纱，但处理冗长的函数（和变量）名使得这个过程变得更加地困难。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-8.jpg" alt=""></p><p>就我个人而言，与查看自定义的变量或者函数名相比，从视觉角度来说，我发现编程语言提供的关键字，构造和习惯用法更加容易接受。例如，当我阅读 if-else 模块时，我很少需要花费精力去处理关键字 if 或者 elseif，只需要花时间理解程序的逻辑流程。</p><p>一个 VeryVeryLongFuncNameAndArgList 名称会中断我的推理思路。当被调用的函数实际上是一个可以轻松内联的单线程时尤其如此。上下文切换很昂贵，不管是 CPU 上下文切换还是程序员在阅读代码时不得不在思想上的上下文切换。</p><p>过度强调小函数的另外一个问题是，尤其是那些描述性很强但名字不直观的函数，在代码库中更难搜索到。相比之下，一个名为 createUser 的函数很容易，且直观地用于 grep，比如 renderPageWithSetupsAndTeardowns（在《Clean Code》中是作为明星例子，这个名字不是最容易记住的名称，也不是最容易搜索到的名字）。许多编辑器也对代码库进行了模糊搜索，因此具有相似前缀的函数也更可能造成搜索时出现多余的结果，这不是我们想要的。</p><h3 id="本地的丢失（Loss-of-Locality）"><a href="#本地的丢失（Loss-of-Locality）" class="headerlink" title="本地的丢失（Loss of Locality）"></a>本地的丢失（Loss of Locality）</h3><p>（注：这里指可以在本函数，本文件，本包中实现的代码，却为了小函数移到了其他的函数，文件，或包中）</p><p>当我们不必跳过文件或包来查找函数的定义时，小函数的效果最好。“Clean Code” 一书为此提出了一个名为 “The Stepdown Rule” 的原则。</p><p>** 我们希望代码能像自上而下的叙述一样容易阅读。我们希望每个函数都被下一级抽象层次的人所遵循，以便我们阅读程序，读取函数列表时，可以一次下降一个抽象层次。我称之为 “The Stepdown Rule”。**</p><p>这个观点理论上可行，但在实际的实践中，却很少能发挥作用。相反的，我看到的大多是在代码中增加更多的函数，减少了本地代码。</p><p>让我们以三个函数 A，B 和 C 的假设开始，一个调用另外一个。我们的初始抽象印证了某些假设、要求和注意事项，所有这些都是我们在最初设计时仔细研究和论证过的。</p><p><img src="https://cdn-images-1.medium.com/max/1600/1*EGR-6c3hu_6joqdNYHfyLQ.png" alt=""></p><p>很快，假设我们有一个新的需求或一个附加功能的情况下，我们需要迎合没有预见的或一个新的约束。我们需要修改函数 A，因为它封装的 “一个整体” 已经不再有效（可能从一个开始就无效，现在我们需要修改它，使它有效）。按照我们在 《Clean Code》中所学到的，我们处理这些问题的最好办法是，创建更多的函数，隐藏掉各种杂七杂八的新需求。</p><p><img src="https://cdn-images-1.medium.com/max/1600/1*N77X5FQhiscmnNUKlR8_Cw.png" alt=""></p><p>我们按照我们的想法修改后，过个几周，如果我们的需求又修改了，我们可能需要创建更多的函数去封装所有要求增加的修改。</p><p><img src="https://cdn-images-1.medium.com/max/1600/1*aQn2iFAvxzrJr_89aTghtQ.png" alt=""></p><p>再来几次，我们就真正的看到了 Sandi Metz 在她的博文 <a href="https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction" target="_blank" rel="noopener">《The Wrong Abstraction》</a> 中描述的问题。这篇博文说：</p><p>已经存在的代码具有强大的影响力。它的存在表明它是正确和有效的。我们知道代码代表了付出的努力，我们非常积极地维护这个努力的价值。不幸的是，可悲的事实是，代码越复杂，越难以理解，即设计它时投入越大，我们就越觉得要保留它（“沉没成本谬论”）。</p><p>如果还是同一团队成员继续维护它，我相信这是对的，但当新的程序员（或经理）获得代码库的所有权时，我会看到相反的结果。以良好意图开始的代码，现在变成了意大利面条的代码，代码不再简洁，成了地狱般的代码，现在 “重构” 或者有时甚至重写代码的冲动更加诱人。</p><p>现在，人们可能会争辩说，从某种程度上说，这是不可避免的。他们是对的。我们很少讨论编写将会退役的代码是多么重要。过去我写过关于使代码在操作上易于退役的重要性，在涉及代码库本身时更是如此。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-9.jpg" alt=""></p><p>通常情况下，程序员只会在代码确定被删除，或者不再使用时，将代码视为 “已死亡”。如果我们开始（以代码将 “死亡”）思考我们的编写的代码，那么每增加一个新的 git commit，我认为我们可能会更加积极地编写易于修改的代码。在思考如何抽象时，认识到我们正在构建的代码可能距离死亡（正在被修改）只有几个小时的事实对于我们很有帮助。因此，为了便于修改代码而进行的优化往往比试图构建 《Clean Code》中提到的自顶向下的设计更好。</p><h2 id="类污染"><a href="#类污染" class="headerlink" title="类污染"></a>类污染</h2><p>在支持面向对象的编程里，小函数带来了更大或者更多的类。在像 Go 一样的编程语言里，我看到这种趋势导致更大的接口（结合接口实现的双重打击）或者大量的小包。</p><p>这加剧了将业务逻辑映射到我们已经创建的抽象认知的开销。类／接口／软件包的数量越多，一举拿下就越困难，这样做证明了我们构建的这些不同类／接口／软件包所需要的维护成本（很大）是合理的。</p><h2 id="更少的参数"><a href="#更少的参数" class="headerlink" title="更少的参数"></a>更少的参数</h2><p>较少函数的支持者几乎总是倾向于支持将更少的参数传递给函数。</p><p>函数参数较少的问题在于，存在依赖关系不清晰的风险。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-10.jpg" alt=""></p><p>我已经看到了 Ruby 类有 5-10 个方法，所有这些方法通常会做一些非常简单的事情，并且可能会有一两个变量作为参数。我也看到他们中的很多人改变了共享的全局变量的状态，或者依赖于没有明确传递关系的单例，只要存在一种情况，就（跟我们之前的讨论的）是一种相反的模式。</p><p>此外，当依赖关系不明确时，测试将变得更加复杂，在针对我们的 itty-bitty 函数的独立测试前，需要重新设置和修改状态值，才能让它运行。</p><h2 id="更难阅读"><a href="#更难阅读" class="headerlink" title="更难阅读"></a>更难阅读</h2><p>这已经在前面陈述过了，但值得重申的是 - 小函数的爆炸式增长，特别是一行的函数，使代码库难以阅读。这尤其会伤害那些代码应该被优化的人 - 新手。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-11.jpg" alt=""></p><p>代码库中有几种类型的新手。根据我的经验，一个好的经验法则是记住某些可能会检查上述 ”新“ 类别的人。这样做可以帮助我重新评估自己的假设，并重新思考我可能会无意中将某些新手加入到第一次阅读代码的新手中。我意识到，这种方法实际上导致比其他方式可能更好更简单的代码。</p><p>简单的代码不代表很容易编写，而且也很少是 DRY 最好的代码。它需要大量细心的思考，关注细节和小心翼翼地达到简单的解决方案，是正确和水到渠成的。这种来之不易的简单性最引人注目的地方在于它适合于新老程序员，易于理解的 ”旧“ 和 ”新“ 的所有可能的定义。</p><p>当我对代码库感到陌生时，如果我有幸已经知道其所使用的语言或者框架时，（那么）对我来说最大的挑战是理解业务逻辑或实现细节。当我不那么幸运时，即面临着（必须）通过用我外行的语言来编写代码库的艰巨任务时，我面临的最大挑战是能够对语言／框架有足够的理解，如履薄冰，以至能够理解代码在做什么而不掉进坑里，同时能够区分出我需要真正理解的，跟目标相关的 ”单一事物“，以便在项目前进中取得必要的进展。</p><p>在这段时间里，我没有看过一个陌生的代码库，所以我会说：</p><p>嗯，这些函数都是足够小，并且符合 DRY 风格的。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-12.jpg" alt=""></p><p>在我尝试寻找问题答案的同时，我冒险进入了未知领域，真正希望的是让最少数量的思维跳跃和上下文切换。</p><p>投入时间和精力，让代码未来的维护者或者消费者变得更容易（理解），这将会产生巨大的回报，特别对于开源项目。这是我希望自己在职业生涯早期做得更好的一件事，而且这段时间我都很注意（这一点）。</p><h2 id="什么情况下小函数有意义"><a href="#什么情况下小函数有意义" class="headerlink" title="什么情况下小函数有意义"></a>什么情况下小函数有意义</h2><p>当所有情况都考虑到了，我相信小函数绝对有它的意义，特别是在测试时。</p><h2 id="网络-I-O"><a href="#网络-I-O" class="headerlink" title="网络 I/O"></a>网络 I/O</h2><p>这不是一篇关于如何最好地为大量服务编写函数，集成和单元测试的文章。然而，当谈到单元测试时，网络 I/O 通过某某方式测试，好吧，实际上没有测试。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-13.jpg" alt=""></p><p>我不是 mock 函数的粉丝。 mock 函数有几个缺点。首先， mock 是一些结果的人工模拟。只有当我们的想象力和我们具备预测我们应用程序可能遇到的各种失败模式的能力时。 mock 也很可能与他们所支持的真实服务不同，除非每个人都对真正的服务进行过严格的测试（注：对细节很了解）。当每个特定模拟只有一个实例并且每个测试使用相同的模拟时， mock 才是最好的。</p><p>也就是说， mock 仍然是单独测试某些形式的网络 I/O 的唯一方法。我们生活在一个微服务时代，并将大部分（如果不是全部的话）关于我们的主要产品的关注都外包给供应商。现在很多应用程序的核心功能都需要一个调用或者多个调用，对这些调用进行单元测试的最佳方法是将其模拟出来。</p><p>总体而言，我发现限制能够 mock 的范围，才是最好的。调用电子邮件服务的 API，以向我们新创建的用户发送欢迎电子邮件，（当然这）需要建立 HTTP 连接。将此请求隔离到尽可能少的函数中，并允许我们在测试中 mock ，以最小化代码量。通常，这应该是一个不超过 1-2 行的函数，用于建立 HTTP 连接并返回任何错误以及响应。将事件发给 Kafka 或在数据库中新创建的用户时也是如此。</p><h2 id="基于属性的测试"><a href="#基于属性的测试" class="headerlink" title="基于属性的测试"></a>基于属性的测试</h2><p>对于那些能够通过这种小代码提供如此巨大利益的东西，基于属性的测试却没有被充分利用起来。（这种测试）由 Haskell 图书馆的 QuickCheck 发明的，并在 Scala（ScalaCheck）和 Python（假设） 等其他语言中被采用，基于属性的测试允许人们生成大量符合给定测试规范的输入，并断言每一个情况的测试通过条件。</p><p>许多基于属性的测试框架都是针对函数的，因此将任何可能受到基于属性测试的东西隔离到单一函数上是有意义的。我发现这在测试数据的编码或解码，测试 JSON 或 msgpack 解析时尤其有用。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>这篇文章的意图既不是说 DRY 也不是说小函数本身就是坏的（尽管本文的标题给出了这样的暗示）。只是说他们本质上没有好坏。</p><p>代码库中的小函数的数量或平均函数长度本身并不是一个可以吹嘘的指标。在 2016 年 PyCon 谈话中有一个名为 onelineizer 的话题，讲述了一个可以将任何 Python 程序（包括它本身）转换为一行代码的同名 Python 程序。虽然这使得会议讨论变得有趣而诱人，但在相同的问题上编写（类似的）产品代码将显得非常愚蠢。</p><p><img src="https://github.com/studygolang/gctt-images/blob/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-14.jpg" alt=""></p><p>上述建议普遍适用，不仅仅对 Go 而言。由于我们编写的程序的复杂性大大增加，而且我们所反对的限制变得更加多变，程序员应该相应地调整他们的思想。</p><p>不幸的是，正统的编程思想依然严重受到面向对象编程和设计模式至高无上的影响。迄今为止，广泛传播的很多想法和最佳实践在很大程度上，几十年以来，一直没有受到过挑战，当前迫切地需要重新思考，尤其是，近年来编程格局和范例已经发生了很大的变化。</p><p>不改变旧的风格不仅会助长懒惰，而且会让程序员陷入他们无法承受的虚假的安抚感中。</p><hr><p>via: <a href="https://medium.com/@copyconstruct/small-functions-considered-harmful-91035d316c29" target="_blank" rel="noopener">https://medium.com/@copyconstruct/small-functions-considered-harmful-91035d316c29</a></p><p>作者：<a href="https://medium.com/@copyconstruct" target="_blank" rel="noopener">Cindy Sridharan</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&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;li&gt;解释一下我认为小函数在 mock 中真正有用的地方
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言中提取字段和方法</title>
    <link href="http://yoursite.com/2018/04/12/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E6%8F%90%E5%8F%96%E5%AD%97%E6%AE%B5%E5%92%8C%E6%96%B9%E6%B3%95/"/>
    <id>http://yoursite.com/2018/04/12/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E6%8F%90%E5%8F%96%E5%AD%97%E6%AE%B5%E5%92%8C%E6%96%B9%E6%B3%95/</id>
    <published>2018-04-12T13:59:43.000Z</published>
    <updated>2020-05-02T14:10:50.359Z</updated>
    
    <content type="html"><![CDATA[<p>struct 是一系列，包含名称和类型的字段。通常就像这样：</p><a id="more"></a><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> &#123;</span><br><span class="line">    name <span class="keyword">string</span></span><br><span class="line">    age <span class="keyword">int32</span></span><br><span class="line">&#125;</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">    person := Person&#123;name: <span class="string">"Michał"</span>, age: <span class="number">29</span>&#125;</span><br><span class="line">    fmt.Println(person)  <span class="comment">// &#123;Michał 29&#125;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>（在这篇博文的接下来部分，我将逐步删除包名、导入和主函数的定义）</p><p>上面的结构体中，每个字段都有明确的名字。Go 语言也允许不指定字段名称。没有名称的字段称为匿名字段或者内嵌字段。类型的名字（如果有包名，不包含这个前缀的包名）就作为字段的名字。因为结构体中要求至少有一个唯一的字段名，所以我们不能这样做：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"net/http"</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">type</span> Request <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"><span class="keyword">type</span> T <span class="keyword">struct</span> &#123;</span><br><span class="line">    http.Request <span class="comment">// field name is "Request"</span></span><br><span class="line">    Request <span class="comment">// field name is "Request"</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></pre></td><td class="code"><pre><span class="line">&gt; go install github.com&#x2F;mlowicki&#x2F;sandbox</span><br><span class="line"># github.com&#x2F;mlowicki&#x2F;sandbox</span><br><span class="line">src&#x2F;github.com&#x2F;mlowicki&#x2F;sandbox&#x2F;sandbox.go:34: duplicate field Request</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><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">type Person struct &#123;</span><br><span class="line">    name string</span><br><span class="line">    age int32</span><br><span class="line">&#125;</span><br><span class="line">func (p Person) IsAdult() bool &#123;</span><br><span class="line">    return p.age &gt;&#x3D; 18</span><br><span class="line">&#125;</span><br><span class="line">type Employee struct &#123;</span><br><span class="line">    position string</span><br><span class="line">&#125;</span><br><span class="line">func (e Employee) IsManager() bool &#123;</span><br><span class="line">    return e.position &#x3D;&#x3D; &quot;manager&quot;</span><br><span class="line">&#125;</span><br><span class="line">type Record struct &#123;</span><br><span class="line">    Person</span><br><span class="line">    Employee</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line">record :&#x3D; Record&#123;&#125;</span><br><span class="line">record.name &#x3D; &quot;Michał&quot;</span><br><span class="line">record.age &#x3D; 29</span><br><span class="line">record.position &#x3D; &quot;software engineer&quot;</span><br><span class="line">fmt.Println(record) &#x2F;&#x2F; &#123;&#123;Michał 29&#125; &#123;software engineer&#125;&#125;</span><br><span class="line">fmt.Println(record.name) &#x2F;&#x2F; Michał</span><br><span class="line">fmt.Println(record.age) &#x2F;&#x2F; 29</span><br><span class="line">fmt.Println(record.position) &#x2F;&#x2F; software engineer</span><br><span class="line">fmt.Println(record.IsAdult()) &#x2F;&#x2F; true</span><br><span class="line">fmt.Println(record.IsManager()) &#x2F;&#x2F; false</span><br></pre></td></tr></table></figure><p>匿名（嵌入）的字段和方法被调用时会自动向上提升（来找到对应的对象）。<br>他们的行为跟常规字段类似，但却不能用于结构体字面值：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//record := Record&#123;&#125;</span></span><br><span class="line">record := Record&#123;name: <span class="string">"Michał"</span>, age: <span class="number">29</span>&#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></pre></td><td class="code"><pre><span class="line">&#x2F;&#x2F; src&#x2F;github.com&#x2F;mlowicki&#x2F;sandbox&#x2F;sandbox.go:23: unknown Record field ‘name’ in struct literal</span><br><span class="line">&#x2F;&#x2F; src&#x2F;github.com&#x2F;mlowicki&#x2F;sandbox&#x2F;sandbox.go:23: unknown Record field ‘age’ in struct literal</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">Record&#123;Person&#123;name: <span class="string">"Michał"</span>, age: <span class="number">29</span>&#125;, Employee&#123;position: <span class="string">"Software Engineer"</span>&#125;&#125;</span><br></pre></td></tr></table></figure><p><a href="https://golang.org/ref/spec#Struct_types" target="_blank" rel="noopener">来源：</a><br><img src="https://github.com/gogeof/gctt-images/blob/master/gogeof/Promoted%20fields%20and%20methods%20in%20Go/promoted-fields-and-methods-in-go-1.jpg?raw=true" alt=""></p><hr><p>via: <a href="https://medium.com/golangspec/promoted-fields-and-methods-in-go-4e8d7aefb3e3" target="_blank" rel="noopener">https://medium.com/golangspec/promoted-fields-and-methods-in-go-4e8d7aefb3e3</a></p><p>作者：<a href="https://medium.com/@mlowicki" target="_blank" rel="noopener">Michał Łowicki</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/校对者ID" target="_blank" rel="noopener">校对者ID</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;struct 是一系列，包含名称和类型的字段。通常就像这样：&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go test 少为人知的特性</title>
    <link href="http://yoursite.com/2018/03/12/%E3%80%90%E8%AF%91%E3%80%91Go-test-%E5%B0%91%E4%B8%BA%E4%BA%BA%E7%9F%A5%E7%9A%84%E7%89%B9%E6%80%A7/"/>
    <id>http://yoursite.com/2018/03/12/%E3%80%90%E8%AF%91%E3%80%91Go-test-%E5%B0%91%E4%B8%BA%E4%BA%BA%E7%9F%A5%E7%9A%84%E7%89%B9%E6%80%A7/</id>
    <published>2018-03-12T13:00:18.000Z</published>
    <updated>2020-05-02T14:12:08.714Z</updated>
    
    <content type="html"><![CDATA[<p>大多数的 Go 程序员都知道和喜欢用 <code>go test</code>，这个测试工具来自于 Go 官方的 <code>gc</code> 工具链。（想要执行测试代码）这个命令可能是最简单的了，而且还能做得很漂亮。</p><a id="more"></a><p>大家都知道，运行 <code>go test</code> 命令将执行当前目录下的包的测试代码，它会寻找 <code>*_test.go</code> 文件，并在这些文件中，寻找符合 <code>TestXxx(*testing.T){}</code> 命名的函数和参数（即，接收 <code>*testing.T</code> 参数的函数，命名为 <code>TestXxx</code>，<code>Xxx</code> 可以是任何不以小写字符开头的名字）。这个测试代码不会影响正常的编译过程，只在执行 <code>go test</code> 时被使用。</p><p>但这里还有很多隐藏的东西。</p><h2 id="黑盒测试包（The-black-box-test-package）"><a href="#黑盒测试包（The-black-box-test-package）" class="headerlink" title="黑盒测试包（The black box test package）"></a>黑盒测试包（The black box test package）</h2><p>通常情况下，在 Go 语言中，测试和要被测试的代码在同一个包中（被测系统），这样才能访问内部实现细节的代码。为了支持黑盒测试，<code>go test</code> 支持使用以 “_test” 后缀命名，并可被编译成独立的包的形式。</p><p>如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// in example.go</span></span><br><span class="line"><span class="keyword">package</span> example</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> start <span class="keyword">int</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Add</span><span class="params">(n <span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">  start += n</span><br><span class="line">  <span class="keyword">return</span> start</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// in example_test.go</span></span><br><span class="line"><span class="keyword">package</span> example_test</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"testing"</span></span><br><span class="line"></span><br><span class="line">  . <span class="string">"bitbucket.org/splice/blog/example"</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">TestAdd</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">  got := Add(<span class="number">1</span>)</span><br><span class="line">  <span class="keyword">if</span> got != <span class="number">1</span> &#123;</span><br><span class="line">    t.Errorf(<span class="string">"got %d, want 1"</span>, got)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你可以在代码中看到臭名昭著的 <a href="https://golang.org/ref/spec#Import_declarations" target="_blank" rel="noopener">点导入</a> 。但当对一个包做黑盒测试时，在当前包的范围内导入（被导入包中）可被导出的符号来说，这是它的一个有实际意义的例子。测试代码<a href="https://code.google.com/p/go-wiki/wiki/CodeReviewComments#Import_Dot" target="_blank" rel="noopener">在通常的情况下应该尽量避免进入被测试的环境中</a>。</p><p>就像在点导入的链接符号章节中所解释的一样，黑盒测试模式也能被用来打破循环导入的问题（在被测试的包 “a” 被 “b” 导入，并且 “a“ 的测试也需要导入 ”b“ 时 - 测试可以被移动到 “a_test“ 包，然后可以（同时）导入 “a” 和 “b”，这样就没有循环导入的问题）。</p><h2 id="跳过测试（Skipping-tests）"><a href="#跳过测试（Skipping-tests）" class="headerlink" title="跳过测试（Skipping tests）"></a>跳过测试（Skipping tests）</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestSomeProtectedResource</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> os.Getenv(<span class="string">"SOME_ACCESS_TOKEN"</span>) == <span class="string">""</span> &#123;</span><br><span class="line">    t.Skip(<span class="string">"skipping test; $SOME_ACCESS_TOKEN not set"</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ... the actual test</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果 <code>go test -v</code> 被调用（注意那个冗余（”-v“）标志），输出将会提醒已跳过的测试：</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></pre></td><td class="code"><pre><span class="line">&#x3D;&#x3D;&#x3D; RUN TestSomeProtectedResource</span><br><span class="line">--- SKIP: TestSomeProtectedResource (0.00 seconds)</span><br><span class="line">    example_test.go:17: skipping test; $SOME_ACCESS_TOKEN not set</span><br></pre></td></tr></table></figure><p>通常是用 <code>-short</code> 命令行标志来实现这个跳过的特性，如果标志被设置的话，反映到代码中，<code>testing.Short()</code> 将简单地返回 true（就像是 <code>-v</code> 标志一样，如果它被设置，通过判断 <code>testing.Verbose()</code> ，你可以打印出额外的调试日志）。</p><p>当测试需要运行较长时间时，而你又很着急的话，你可以执行 <code>go test -short</code>，（如果）提供这个包的开发者又刚好实现了这个功能，运行时间长的测试将会被跳过。这就是从源码安装时，（通常情况下）Go 测试被执行的样子，这里有 stdlib 库中运行时间较长的测试被跳过的例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestCountMallocs</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> testing.Short() &#123;</span><br><span class="line">    t.Skip(<span class="string">"skipping malloc count in short mode"</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// rest of test...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>跳过只是一个可选项，<code>-short</code> 标志只是一个标示，具体还依赖于开发者，他们可以选择（这种标示生效时是否）运行的测试，来避免一些运行比较慢的断言被执行。</p><p>这里还有 <code>-timeout</code> 标志，它能够被用来强制退出限定时间内没有运行完的测试。例如，运行这个命令 <code>go test -timeout 1s</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="function"><span class="keyword">func</span> <span class="title">TestWillTimeout</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">  time.Sleep(<span class="number">2</span> * time.Second)</span><br><span class="line">  <span class="comment">// pass if timeout &gt; 2s</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></pre></td><td class="code"><pre><span class="line">&#x3D;&#x3D;&#x3D; RUN TestWillTimeout</span><br><span class="line">panic: test timed out after 1s</span><br></pre></td></tr></table></figure><p>如果想执行特定的测试函数，而不是执行全部的测试集，只需要运行 <code>go test -run TestNameRegexp</code>。</p><h3 id="并行执行测试（Parallelizing-tests）"><a href="#并行执行测试（Parallelizing-tests）" class="headerlink" title="并行执行测试（Parallelizing tests）"></a>并行执行测试（Parallelizing tests）</h3><p>默认情况下，指定包的测试是按照顺序执行的，但也可以通过在测试的函数内部使用 <code>t.Parallel()</code> 来标志某些测试也可以被安全的并发执行（和默认的一样，假设参数名为 <code>t</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="function"><span class="keyword">func</span> <span class="title">TestParallel</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">  t.Parallel()</span><br><span class="line">  <span class="comment">// actual test...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在并发情况下，同时运行的测试的数量默认取决于 <code>GOMAXPROCS</code>。它可以通过 <code>-parallel n</code> 被指定（<code>go test -parallel 4</code>）</p><p>另外一个可以实现并行的方法，尽管不是函数级粒度，但却是包级粒度，就是类似这样执行 <code>go test p1 p2 p3</code>（也就是说，同时调用多个测试包）。在这种情况下，包会被先编译，并同时被执行。当然，这对于总的时间来说是有好处的，但它也可能会导致错误变得具有不可预测性，比如一些资源被多个包同时使用时（例如，一些测试需要访问数据库，并删除一些行，而这些行又刚好被其他的测试包使用的话）。</p><p>为了保持可控性，<code>-p</code> 标志可以用来指定编译和测试的并发数。当仓库中有多个测试包，并且每个包在不同的子目录中，一个可以执行所有包的命令是 <code>go test ./...</code>，这包含当前目录和所有子目录。没有带 <code>-p</code> 标志执行时，总的运行时间应该接近于运行时间最长的包的时间（加上编译时间）。运行 <code>go test -p 1 ./...</code>，使编译和测试工具只能在一个包中执行时，总的时间应该接近于所有独立的包测试的时间加上编译的时间的总和。你可以自己试试，执行 <code>go test -p 3 ./...</code>，看一下对运行时间的影响。</p><p>还有，另外一个可以并行化的地方（你应该测试一下）是在包的代码里面。多亏了 Go 非常棒的并行原语，实际上，除非 GOMAXPROCS 通过环境变量或者在代码中显式设置为 GOMAXPROCS=1，否则，包中一个goroutines 都没有用是不太常见的。想要使用 2 个 CPU，可以执行 <code>GOMAXPROCS=2 go test</code>，想要使用 4 个 CPU，可以执行 <code>GOMAXPROCS=4 go test</code>，但还有更好的方法：<code>go test -cpu=1,2,4</code> 将会执行 3 次，其中 GOMAXPROCS 值分别为 1，2，和 4。</p><p><code>-cpu</code> 标志，搭配数据竞争的探测标志 <code>-race</code>，简直进入天堂（或者下地狱，取决于它具体怎么运行）。竞争探测是一个很神奇的工具，在以高并发为主的开发中不得不使用这个工具（来防止死锁问题），但对它的讨论已经超过了本文的范围。如果你对此感兴趣，可以阅读 Go 官方博客的 <a href="http://blog.golang.org/race-detector" target="_blank" rel="noopener">这篇文章</a>。</p><h2 id="更多的内容"><a href="#更多的内容" class="headerlink" title="更多的内容"></a>更多的内容</h2><p><code>go test</code> 工具支持以与测试函数相似的方式运行基准测试和可断言示例（！）。<code>godoc</code> 工具甚至能够理解例子中的语法并将其包含在生成的文档中。</p><p>不得不提的还有代码覆盖率和性能测试，测试工具也支持这两个功能。对于感兴趣并想要深入了解的，可以访问 <a href="http://blog.golang.org/cover" target="_blank" rel="noopener">The cover story</a> 和 <a href="http://blog.golang.org/profiling-go-programs" target="_blank" rel="noopener">Profiling Go programs</a>，它们都在 Go 博客中。</p><p>在你写自己的测试代码前，建议看一下标准库中的 <code>testing/iotest</code>，<code>testing/quick</code> 和 <code>net/http/httptest</code> 软件包。</p><hr><p>via: <a href="https://splice.com/blog/lesser-known-features-go-test/" target="_blank" rel="noopener">https://splice.com/blog/lesser-known-features-go-test/</a></p><p>作者：<a href="https://splice.com/blog/author/martin/" target="_blank" rel="noopener">MARTIN ANGERS</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;大多数的 Go 程序员都知道和喜欢用 &lt;code&gt;go test&lt;/code&gt;，这个测试工具来自于 Go 官方的 &lt;code&gt;gc&lt;/code&gt; 工具链。（想要执行测试代码）这个命令可能是最简单的了，而且还能做得很漂亮。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】关于在 Go 代码中使用退避方法，啰嗦几句</title>
    <link href="http://yoursite.com/2018/03/06/%E3%80%90%E8%AF%91%E3%80%91%E5%85%B3%E4%BA%8E%E5%9C%A8-Go-%E4%BB%A3%E7%A0%81%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%80%80%E9%81%BF%E6%96%B9%E6%B3%95%EF%BC%8C%E5%95%B0%E5%97%A6%E5%87%A0%E5%8F%A5/"/>
    <id>http://yoursite.com/2018/03/06/%E3%80%90%E8%AF%91%E3%80%91%E5%85%B3%E4%BA%8E%E5%9C%A8-Go-%E4%BB%A3%E7%A0%81%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%80%80%E9%81%BF%E6%96%B9%E6%B3%95%EF%BC%8C%E5%95%B0%E5%97%A6%E5%87%A0%E5%8F%A5/</id>
    <published>2018-03-06T12:57:27.000Z</published>
    <updated>2020-05-02T14:09:12.359Z</updated>
    
    <content type="html"><![CDATA[<p>我相信你有过调用 API 接口需要使用退避算法的时候。在 Go 语言现有技术中，有 <a href="https://github.com/cenkalti/backoff" target="_blank" rel="noopener">github.com/cenkalti/backoff</a>，<a href="https://github.com/jpillora/backoff" target="_blank" rel="noopener">github.com/jpillora/backoff</a>，和其它库可以使用。</p><a id="more"></a><p>我曾经使用过 <a href="https://github.com/cenkalti/backoff" target="_blank" rel="noopener">github.com/cenkalti/backoff</a>，但是有一件让我感到困惑的事：它要求你为操作加上闭包，强制输入为 func() error 的形式。</p><p>举个例子，当你需要一个可以自动重试的函数（如下面的 myFunc 函数），返回 3 个值和一个 error，你需要和作用域斗智斗勇。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a, b, c Result</span><br><span class="line">backoff.Retry(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> err error</span><br><span class="line">  a, b, c, err = myFunc(arg, ...)</span><br><span class="line">  <span class="keyword">return</span> err</span><br><span class="line">&#125;, backoffObject)</span><br></pre></td></tr></table></figure><p>在性能方面，这可能可以忽略不计，但作用域全搞乱了，这个时候你必须要小心，在赋值时不要使用 :=，而是使用 = 来确保获得正确的值。</p><p>这是我写 <a href="github.com/lestrrat-go/backoff">github.com/lestrrat-go/backoff</a> 库的主要原因。</p><p>使用这个库，你将不得不写更多的样板代码（使用库需要执行的函数：NewExponential()，Start() 等），但是你不需要实现一个闭包，我发现这样更加符合 Go 的风格。计算退避持续时间仍然很难，如果你有更好的算法，请告诉我。</p><p>使用 <a href="github.com/lestrrat-go/backoff">github.com/lestrrat-go/backoff</a> 库，首先你要创建一个策略对象：</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">policy := backoff.NewExponential(...)</span><br></pre></td></tr></table></figure><p>传入的参数也会影响到策略，例如配置最大重试次数或者不重试。策略对象可以被多个消费者重用。</p><p>实际上 backoff 对象是通过调用策略对象的 Start() 方法创建的：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">b, cancel := policy.Start(ctx)</span><br></pre></td></tr></table></figure><p>该方法接收一个 context 对象，因此你可以通过父作用域内的释放操作终止退避算法。</p><p>当退避对象 b 不再需要时，cancel 用于释放资源。</p><p>退避对象 b 包含两个方法，Done() 和 Next()。他们都返回一个可以通知我们事件的管道变量。</p><p>当退避算法停止时，Done() 变得可读：（停止的情况）包括父 context 被取消导致退避算法被取消或者某个条件发生（例如当我们已经重试了 MaxRetries 参数指定的次数时）。</p><p>当调用过了足够的时候后，Next() 才变为可读。在指数级回退的情况下，在调用 Next() 之后，你可能得等 1， 2， 4， 8， 16…（乘以基本区间）。</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="function"><span class="keyword">func</span> <span class="title">MyFuncWithRetries</span><span class="params">(ctx context.Context, ... )</span> <span class="params">(Result1, Result2, Result3, error)</span></span> &#123;</span><br><span class="line">  b, cancel := policy.Start(ctx)</span><br><span class="line">  <span class="keyword">defer</span> cancel()</span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    ret1, ret2, ret3, err := MyFunc(...)</span><br><span class="line">    <span class="keyword">if</span> err == <span class="literal">nil</span> &#123; <span class="comment">// success</span></span><br><span class="line">      <span class="keyword">return</span> ret1, ret2, ret3, <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-b.Done():</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, errors.New(<span class="string">`all attempts failed`</span>)</span><br><span class="line">    <span class="keyword">case</span> &lt;-b.Next():</span><br><span class="line">      <span class="comment">// continue to beginning of the for loop, execute MyFunc again</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>我认为这是一个关于口味的问题，所以你应该选择符合你口味或者习惯的库。这恰好是我想要的库，希望也对你有帮助。</p><p>编程快乐！</p><p>via: <a href="https://medium.com/@lestrrat/yak-shaving-with-backoff-libraries-in-go-80240f0aa30c" target="_blank" rel="noopener">https://medium.com/@lestrrat/yak-shaving-with-backoff-libraries-in-go-80240f0aa30c</a></p><p>作者：<a href="https://github.com/lestrrat" target="_blank" rel="noopener">Daisuke Maki</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/rxcai" target="_blank" rel="noopener">rxcai</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;我相信你有过调用 API 接口需要使用退避算法的时候。在 Go 语言现有技术中，有 &lt;a href=&quot;https://github.com/cenkalti/backoff&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/cenkalti/backoff&lt;/a&gt;，&lt;a href=&quot;https://github.com/jpillora/backoff&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/jpillora/backoff&lt;/a&gt;，和其它库可以使用。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】曾经的 Go 程序员又回来了</title>
    <link href="http://yoursite.com/2018/03/04/%E3%80%90%E8%AF%91%E3%80%91%E6%9B%BE%E7%BB%8F%E7%9A%84-Go-%E7%A8%8B%E5%BA%8F%E5%91%98%E5%8F%88%E5%9B%9E%E6%9D%A5%E4%BA%86/"/>
    <id>http://yoursite.com/2018/03/04/%E3%80%90%E8%AF%91%E3%80%91%E6%9B%BE%E7%BB%8F%E7%9A%84-Go-%E7%A8%8B%E5%BA%8F%E5%91%98%E5%8F%88%E5%9B%9E%E6%9D%A5%E4%BA%86/</id>
    <published>2018-03-04T12:59:00.000Z</published>
    <updated>2020-05-02T14:09:26.710Z</updated>
    
    <content type="html"><![CDATA[<p>按照惯例，第一篇博文总要说点历史，对吧？我作为软件开发人员，主要还是使用 PHP 语言。第一次接触它还是我在高中的时候，跟一个朋友一起开发一个项目。我们想为项目建一个网站，正好发现一个用 PHP 语言，代码写得非常整齐，并且预打包的解决方案。那个时候，我完全不懂这些代码是干嘛的，也没有花时间去弄懂它。多年之后，我再次与 PHP 相遇。在我进入大学的前 6 个月（我想大约 7 年前），PHP 成了我主要关注的语言之一。两年前，我第一次经人介绍了解到 Go 语言。</p><a id="more"></a><p>我大部分专业经历都来自于位于利兹中心的代理机构。这是一个梦幻般的公司，我从中学到了很多，因为很早我就被给予了很高的自由度。我为一个范围很广的项目工作：标准的 CMS 构建，定制 API 接口，手机应用程序，为了提高效率的自动化，部署和基础架构设计，以及其他很多功能。</p><p>在代理机构的这段时间里，我和一个我们聘请的实习生成为了朋友。他真的是个天才，也是他突然向我介绍 Go 语言。我说我会试试，然后说一下我对它的看法。很快，我就觉得我喜欢上它了。跟 PHP 相比，更好的性能，类型安全，支持并发，本地编译后的二进制文件，相当小，启动速度相当快，这非常地诱人！如果你从 PHP 转 Go 开发，那么会有一大堆让你感到激动的东西。</p><p>奇怪的是，我其实并没有那么喜欢它。我认为它的语法很怪，它的工具还好，但处理错误是如此地繁琐，（甚至，）天哪，地球上怎么还有像 Go 语言这样没有支持泛型的语言？我记得与这位实习生进行了很多类似的对话，他可能告诉我所有我后来自己也慢慢意识到的事情，但是我继续寻找新的东西来学习 PHP 以外的东西，现在改为 Go 语言也是这样。</p><h2 id="使用-Scala-的一年（The-Year-of-Scala）"><a href="#使用-Scala-的一年（The-Year-of-Scala）" class="headerlink" title="使用 Scala 的一年（The Year of Scala）"></a>使用 Scala 的一年（The Year of Scala）</h2><p>我的搜索很快把我带到了 Scala 的世界。如果你没怎么听过 Scala 的话，它是运行在 JVM 上的函数编程语言。当时很有吸引力的一点是 JVM 很快（一旦启动并运行起来之后），函数式编程当时疯狂地席卷了编程界。</p><p>我迷上了 Scala，花了一年时间围绕着它，学习它的语法。当我爱上它时，尽管编译时间比 Go 长，但使用 SBT 的工具实际上也没有那么糟糕（得益于增量编译）。我学习其语法，花了一些时间之后，就能像编写 Java 一样编写 Scala，而且使用的语法还更少一些。接下来，我专注于学习函数编程原理，如更纯粹，引用透明性，不修改状态（等函数式编程的原则）。在编写 PHP 时，我曾遵循过许多这里描述的原则，但并没有想过要如何编写一个纯函数式的应用程序。</p><p>在我使用 Scala 的过程中，我大量阅读了相关博客，阅读了一些 Scala 社区里比较流行的书，参加 Martin Odersky 使用 Scala 函数式编程的课程。我觉得我不能只是待在我能做出有用的东西的地方。我应该待在可以提高在 Scala 项目工作的机会的地方，我对自己的 Scala 技能应该有足够的信心，我应该能为这样的项目工作。然而情况却是…我并不这么觉得。我还是觉得其他人的代码很让人迷惑，很难阅读。觉得其他人写的每一个库都有自己不同的（编程）风格，并且还需要学习大量的词汇。我了解一些广泛被使用的库，像 <a href="https://typelevel.org/cats/" target="_blank" rel="noopener">cats</a> 和 <a href="https://github.com/milessabin/shapeless" target="_blank" rel="noopener">shapeless</a>，但还是不明白它们干了什么或者为什么你需要它们。</p><p>回过头来，不要误解我的意思。Scala 是一门令人印象深刻的语言，我非常尊重那些使用这门语言工作的人。它本身没有任何限制，这是好事也是坏事（对于我来说，是坏事）。我喜欢类似于保持不变的想法，难以置信的是编译器捡起了类型错误。这非常罕见，它丢掉了重要的东西，并让错误可以在运行时发生。</p><p>我确实用 Scala 做了一些事情，但也弄得我越来越沮丧。我曾想解决我想解决的问题，但总是因为各种原因弄得焦头烂额。要么就是我缺乏函数编程的经验技巧，要么就是像 JVM 启动时间这样的事情让我感到沮丧。不知道为什么，我在这一年后仍然发现还有 Scala 的新语法（需要我去学）。我觉得我已经在这条路上走了一年，但收获甚少。</p><h2 id="重回-Go-语言（Back-to-Go-Again）"><a href="#重回-Go-语言（Back-to-Go-Again）" class="headerlink" title="重回 Go 语言（Back to Go Again）"></a>重回 Go 语言（Back to Go Again）</h2><p>在使用 Scala 一年以后，我决定再给 Go 一个机会。（因为）我发现没有其他的语言能像 Go 一样在方框中打这么多勾，使用 Scala 的一年只是让我增加更多的方框来打勾。这些方框都有什么呢？</p><ul><li>快速编译：虽然 SBT 增量式编译在开发时可以加快编译速度，从头开始编译还是需要花费很长的时间，而且即使是 SBT 增量式编译，花的时间也比 <code>go build</code> 花的时间长。</li><li>快速启动应用：我对于开发 CLI 工具非常感兴趣。早期使用 Go 和 Scala 语言开发的库中都有一个为了创建 CLI 应用的库。这对于用来测试不同语言开发的应用的启动速度非常有用。</li><li>低内存占用：我并不是研究 JVM 直接使用多少内存的狂热粉丝。但我也知道如果我想将我做的事情 Docker 化，对于今天的 JVM 来说还是个问题。</li><li>功能丰富，风格一致，但简单的标准库：Scala 拥有大量的库，但你可以发现（其实）大量依赖于 Java 库。有一些并不是那么令人愉快。最重要的是，当你开始引入这些依赖关系时，心里就会开始觉得（为啥）你不得不下载整个互联网来做一些非常基本的事情。</li><li>编译时类型检查：Scala 在这方面表现出色，但多数情况下，你需要牺牲可读性作为代价。在某些情况下，Go 会漏掉一些编译时的类型检查，但这种情况非常罕见，因为通常有其他可以在编译时做类型检查的解决方案。</li><li>垃圾收集和类型安全：我不想接触太低层的东西，但我还是想比我使用 PHP 时能对应用程序干什么有更多的控制。</li><li>内嵌的并发原语：来自于 PHP，这是我想更多解释的地方。使用 Scala 开发程序时，我有机会用上并发，但从来没有像使用 Go 一样对应用程序能控制得这么多。</li><li>广泛采用的编程风格约定：来自于 PHP，PHP 标准建议（PSR）相当流行，我知道我喜欢拥有能够到处使用的代码风格约定的语言。我相信很多语言都能从中受益，包括 Scala。</li></ul><p>对于我来说，Go 符合所有这些要求，甚至更多。我认为 Go 给了你很多可以开箱即用的东西。你也可以在其他地方找到这些东西，但并不像 Go 一样全，而且整洁。</p><p>开始激怒我的一些 Go 的特性也变得可以接受了。我觉得我最终明白了一些东西，像 Go 语言一样，也需要作出一些妥协；以确保像上面的那些框可以被打勾。为了你可以得到更多的好处，另外一些地方就得难受一点，但有一点非常清晰，语言总体上被设计成最大化开发生产效率。错误控制就是个很好的例子。它很繁琐，有时它有一点不是那么有用。然而，它让函数里面哪里会发生错误变得非常清晰，这意味着，当你阅读别人代码的时候，你能够清楚地知道什么时候，或者为什么错误会发生。</p><p>另外一个可以做出这个判断的，开始时令我感到困惑的是 <code>context</code> 包。我本来并不是很喜欢它，并且认为它处理问题用了一种非常冗长的方式。为什么我们不能直接杀掉 goroutines 呢？<code>context</code> 跟错误非常像，它的冗长也是显式的，告诉你某个给定代码的某些内容很可能在某些时候被取消。你不能杀掉 goroutine，因为你没办法处理它产生的 goroutine?毕竟他们不再是孩子。</p><p>约定，工具使得 Go 语言开发的项目令人难以置信地一致。到当前为止，比我用过的任何语言都要好。当你有像 <code>goimports</code> 和 <code>gometalinter</code> 一样的工具，就不奇怪每一个项目都有相同的代码风格，甚至相同的文档风格。工具也能帮助你避免代码中相同的 bug 和减少误解。</p><p>我开始用 Go 写软件后，觉得最棒的是，我可以真正解决问题了。我想解决的实际问题，不仅仅是一些随机的数学教学问题。最近一段时间（我希望可以写得很快），我在制作一些工具来帮助自动化我的 Arch Linux 桌面的一些地方。我写了一些工具去管理我的工作区，为一些东西显示通知，做一些像自动配置我的显示器（都是一些解决桌面环境的问题，但它是一切工作的开始）。</p><p>学习 Scala 时，对于一些数学问题，我建议创建的解决方案非常有意思，但却（纠结于）没能写出解决实际问题的软件，直到使用 Go 语言才释然。这不是 Scala 的错误，它确实能够解决问题。我知道这是一个巨大的范式转变，鉴于我的历史，我将需要一个很大半径的弯曲学习曲线。对于我来说 Go 没有学习门槛；实际上 Go 本来压根就没有学习门槛。没有很复杂的编译工具，语法可以一下子装进你的脑里，你能很快上手。可能你从第一天开始就可以阅读别人的代码，包括阅读标准库中的代码。</p><p>Go 语言不是完美的，但它在不断完善中。我对这门语言的未来感到非常兴奋，并且从中看到了我想要的东西。</p><hr><p>via: <a href="https://www.elliotdwright.com/2018/02/26/there-and-back-again/" target="_blank" rel="noopener">https://www.elliotdwright.com/2018/02/26/there-and-back-again/</a></p><p>作者：<a href="https://github.com/seeruk" target="_blank" rel="noopener">Elliot Wright</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;按照惯例，第一篇博文总要说点历史，对吧？我作为软件开发人员，主要还是使用 PHP 语言。第一次接触它还是我在高中的时候，跟一个朋友一起开发一个项目。我们想为项目建一个网站，正好发现一个用 PHP 语言，代码写得非常整齐，并且预打包的解决方案。那个时候，我完全不懂这些代码是干嘛的，也没有花时间去弄懂它。多年之后，我再次与 PHP 相遇。在我进入大学的前 6 个月（我想大约 7 年前），PHP 成了我主要关注的语言之一。两年前，我第一次经人介绍了解到 Go 语言。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言机制之数据和语法的设计哲学</title>
    <link href="http://yoursite.com/2018/02/28/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E6%95%B0%E6%8D%AE%E5%92%8C%E8%AF%AD%E6%B3%95%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6/"/>
    <id>http://yoursite.com/2018/02/28/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E6%95%B0%E6%8D%AE%E5%92%8C%E8%AF%AD%E6%B3%95%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6/</id>
    <published>2018-02-28T12:55:53.000Z</published>
    <updated>2020-05-02T14:10:37.459Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前序（Prelude）"><a href="#前序（Prelude）" class="headerlink" title="前序（Prelude）"></a>前序（Prelude）</h2><p>本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值或者指针传递。这是最后一篇，重点介绍在代码中使用值和指针的数据和语义的设计哲学。</p><a id="more"></a><p>以下是本系列文章的索引：</p><p>1) <a href="https://www.goinggo.net/2017/05/language-mechanics-on-stacks-and-pointers.html" target="_blank" rel="noopener">Go 语言机制之栈与指针</a><br>2) <a href="https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html" target="_blank" rel="noopener">Go 语言机制之逃逸分析</a><br>3) <a href="https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html" target="_blank" rel="noopener">Go 语言机制之内存剖析</a><br>4) <a href="https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html" target="_blank" rel="noopener">Go 语言机制之数据和语法的设计哲学</a></p><h2 id="设计哲学（Design-Philosophies）"><a href="#设计哲学（Design-Philosophies）" class="headerlink" title="设计哲学（Design Philosophies）"></a>设计哲学（Design Philosophies）</h2><p>“在栈上保存值，这减少了垃圾收集器（GC）的压力。然而，却要求存储、跟踪和维护给定值的多个副本。将值放在堆上，这会给 GC 增加压力。但是它也是有用的，因为只需要针对一个值进行存储、跟踪和维护。” - Bill Kennedy</p><p>对于给定类型的数据，想在整个软件中保持完整性和可读性，使用值或者指针要保持一致。为什么？因为，如果你在函数间传递数据时修改数据语义，将很难维护一个清晰一致的心智模型。代码库和团队越大，越多的 bug、对数据的竞争和其他副作用就会悄悄地潜入到代码库中。</p><p>我想从一组设计哲学开始讨论，它将指导（我们如何）选择一种语义而不是另外一种语义的方法。</p><h2 id="心智模型（Mental-Models）（译者注：心智模型是经由经验及学习，脑海中对某些事物发展的过程，所写下的剧本。可以当成对代码整体的把控）"><a href="#心智模型（Mental-Models）（译者注：心智模型是经由经验及学习，脑海中对某些事物发展的过程，所写下的剧本。可以当成对代码整体的把控）" class="headerlink" title="心智模型（Mental Models）（译者注：心智模型是经由经验及学习，脑海中对某些事物发展的过程，所写下的剧本。可以当成对代码整体的把控）"></a>心智模型（Mental Models）（译者注：心智模型是经由经验及学习，脑海中对某些事物发展的过程，所写下的剧本。可以当成对代码整体的把控）</h2><p>“让我们想象有这样一个项目，它包含一百万行以上的代码量。这些项目当前在美国能成功的可能性很低，远低于 50%。或许有人不同意这个说法。” - Tom Love (inventor of Objective C)</p><p>Tom 还说一盒复印纸可以容纳 10 万行代码。稍微想一下。你能掌控这个盒子中的代码的百分之多少呢？</p><p>我相信要一个开发人员维护一张纸上的代码的心智模型（大约 1 万行代码）已经是个问题。但是，我们还是假设每个开发人员开发 1 万行代码，那么需要由 100 位开发人员组成的团队来维护一个包含 100 万行代码的代码库。也就是说 100 人需要协调，分组，跟踪和不断沟通。现在，再看看你们 1 到 10 名开发人员组成的团队。你们在这个小得多的规模做得如何？假设每人 1 万行代码，（你们）团队规模与代码库的大小是否相符？</p><h2 id="调试（Debugging）"><a href="#调试（Debugging）" class="headerlink" title="调试（Debugging）"></a>调试（Debugging）</h2><p>“最大的问题是你的心智模型是错误的，所以你根本找不到问题所在。” - Brian Kernighan</p><p>我不相信，你能在没有心智模型的基础上，使用调试器解决问题，你只不过是在浪费时间精力尝试理解问题。</p><p>如果你在生产环境中遇到问题，你能问谁？没错，日志。如果日志在你开发过程中对你没有用，那么当生产环境上出问题，它也一定对你没有用。日志应该基于代码的心智模型，这样才能通过阅读代码找到问题所在。</p><h2 id="可读性（Readability）"><a href="#可读性（Readability）" class="headerlink" title="可读性（Readability）"></a>可读性（Readability）</h2><p>C 语言是我见过的在性能和表达性上平衡得最好的。你可以通过简单的编程实现任何你想要做的事情，并且你会对机器即将要发生的事情拥有一个非常好的心智模型。你可以非常合理地预测它的速度，你知道即将要发生什么…” - Brian Kernighan</p><p>我相信 Brian 这句话也适用于 Go。保持这种 “心智模型” 就是一切。它驱动完整性，可读性和简单性。这些是精心编写的软件的基石，使得它可以保持正常并持续运行下去。编写保证给定类型数据的值或者指针语义一致的代码是实现这一点的重要方法。</p><h2 id="面向数据设计（Data-Oriented-Design）"><a href="#面向数据设计（Data-Oriented-Design）" class="headerlink" title="面向数据设计（Data Oriented Design）"></a>面向数据设计（Data Oriented Design）</h2><p>“如果你不了解这些数据，你就不明白这个问题。因为所有的问题都是独特的，并且与你所使用的数据关系紧密。当数据发生变化时，你的问题也会跟着变化。但问题发生变化时，你的算法（数据转换）也需要跟着变化。” - Bill Kennedy</p><p>想一想。你解决问题的方法实际上是解决数据转换的问题。你写的每个函数，运行的每个程序，（只不过）都是获取一些输入数据，产生一些输出数据。从这个角度看，你的软件的心智模型就是对这些数据转换的理解（例如，如何在代码中组织和使用它们）。”少即是多” 的原则对于解决问题时实现较少的层数，代码量，迭代次数，以及降低复杂性和减少工作量非常重要。</p><h2 id="类型（就是生命）（Type-Is-Life-）"><a href="#类型（就是生命）（Type-Is-Life-）" class="headerlink" title="类型（就是生命）（Type (Is Life)）"></a>类型（就是生命）（Type (Is Life)）</h2><p>“完整性意味着每次分配内存，读取内存和写入内存都是准确，一致和高效的。类型系统对于我们具有这种微观完整性至关重要。” - William Kennedy</p><p>如果数据驱动你所做的一切，那么代表数据的类型就十分地重要。在我的观点里面 “类型就是生命”，因为类型为编译器提供了确保数据完整性的能力。类型也驱动并指示语义规则，程序必须遵循其所操作的数据的语义。这是正确地使用值或者指针语义的开始：使用类型。</p><h2 id="数据（的能力）"><a href="#数据（的能力）" class="headerlink" title="数据（的能力）"></a>数据（的能力）</h2><p>“当数据是实际和合理的，方法才是有效的。” - William Kennedy</p><p>值或者指针语义的思想不会直接影响 Go 开发人员，除非他们需要决定方法接收值还是指针。这是我遇到的一个问题：我应该使用值作为参数还是指针？一听到这个问题，我就知道这个开发人员没有理解好这些（类型的）语义。</p><p>方法的目的是使这些数据具有某种能力。想象一下，数据有能力做某些事情。我总是希望把重点放在数据上，因为它驱动程序的功能。数据驱动你写的算法，封装和能达到的性能。</p><h2 id="多态（Polymorphism）"><a href="#多态（Polymorphism）" class="headerlink" title="多态（Polymorphism）"></a>多态（Polymorphism）</h2><p>“多态意味着你写了一个特定的程序，但它的行为有所不同，具体取决于它所操作的数据。” - Tom Kurtz (inventor of BASIC)</p><p>我很喜欢 Tom 上面说的话。函数的行为可以根据操作的数据的不同而不同。这个数据的行为是将函数从它们可以接受和使用的具体数据类型中分离出来的，这是数据可以具有某种能力的原因。这个观点是使得架构和设计可以适应变化的系统的基石。</p><h2 id="原型的第一种方法（Prototype-First-Approach）"><a href="#原型的第一种方法（Prototype-First-Approach）" class="headerlink" title="原型的第一种方法（Prototype First Approach）"></a>原型的第一种方法（Prototype First Approach）</h2><p>“除非开发人员对软件会被如何使用有一个很好的了解，否则软件很可能会出问题。如果开发人员不是很了解或者对软件不是很理解，那么获得尽可能多的用户输入和用户级测试就相当的重要。” - Brian Kernighan</p><p>我希望你始终专注于理解具体的数据和为了解决问题所需要的数据转换的算法。采用这种原型的第一种方法，编写也可以在生产环境中部署的具体实现（如果这样做是合理和实际的话）。一旦一个具体的实现已经能够工作，一旦你已经知道哪些工作起作用，哪些不起作用，就应该关注于重构，将实现与具体数据分离，将之赋予数据以能力（译者注：我的理解，简单地说，就是抽象为数据类型的一个方法）。</p><h2 id="语义原则（Semantic-Guidelines）"><a href="#语义原则（Semantic-Guidelines）" class="headerlink" title="语义原则（Semantic Guidelines）"></a>语义原则（Semantic Guidelines）</h2><p>你在声明类型时，必须决定特定数据类型将使用哪种语义，值或者指针。接收或返回该类型数据的 API 必须遵循为该类型选择的语义。API 不允许（用户）指定或改变语义。他们必须知道数据使用什么语义，并符合这一点。这是实现大型代码库一致性的起码要求。</p><p>以下是基本指导原则：</p><ul><li>当你声明一个类型时，你必须决定所使用的语义</li><li>函数和方法必须遵循给定类型所选择的语义</li><li>避免让方法接收与给定类型相对应的不同语义</li><li>避免函数接收或者返回与给定类型相对应的不同语义</li><li>避免改变给定类型的语义</li></ul><p>这些指导原则有一些例外的情况，最大的是 unmarshaling。Unmarshaling 总是需要使用指针语义。Marshaling 和 unmarshaling 似乎总是例外的规则。</p><p>你如何选择一种给定类型的一种语义而不是另外一种？这些指导方针将回答这个问题。以下我们将在具体的情况下使用指导原则：</p><h2 id="内置类型"><a href="#内置类型" class="headerlink" title="内置类型"></a>内置类型</h2><p>Go 语言中内置类型包括数字，文本和布尔类型。这些类型应该使用值语义进行处理。除非你有非常好的理由，否则不要使用指针来共享这些类型的值。</p><p>作为一个例子，从 strings 包中查看这些函数的声明。</p><h3 id="代码清单-1"><a href="#代码清单-1" class="headerlink" title="代码清单 1"></a>代码清单 1</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Replace</span><span class="params">(s, old, <span class="built_in">new</span> <span class="keyword">string</span>, n <span class="keyword">int</span>)</span> <span class="title">string</span></span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">LastIndex</span><span class="params">(s, sep <span class="keyword">string</span>)</span> <span class="title">int</span></span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ContainsRune</span><span class="params">(s <span class="keyword">string</span>, r <span class="keyword">rune</span>)</span> <span class="title">bool</span></span></span><br></pre></td></tr></table></figure><p>所有这些函数在 API 设置中都使用值语义。</p><h2 id="引用类型"><a href="#引用类型" class="headerlink" title="引用类型"></a>引用类型</h2><p>Go 语言中引用类型包括切片，map，接口，函数和 channel。这些类型建议使用值语义，因为它们被设计成待在栈中以最小化堆的压力。它们允许每个函数都有自己的值副本，而不是每个函数都会造成潜在的分配。这是可能的，因为这些值包含一个在调用之间共享底层数据结构的指针。</p><p>除非你有很好的理由，否则不要用指针共享这些类型的值。将调用栈中的 map 或 slice 共享给 Unmarshal 函数可能是一个例外。作为一个例子，看看 net 库上声明的这两种类型。</p><h3 id="代码清单-2"><a href="#代码清单-2" class="headerlink" title="代码清单 2"></a>代码清单 2</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> IP []<span class="keyword">byte</span></span><br><span class="line"><span class="keyword">type</span> IPMask []<span class="keyword">byte</span></span><br></pre></td></tr></table></figure><p>IP 和 IPMask 都是字节切片。这意味着它们是引用类型，并且它们应该要符合值语义。下面是一个名叫 Mask 的方法，它被声明为接收一个 IPMask 值的 IP 类型。</p><h3 id="代码清单-3"><a href="#代码清单-3" class="headerlink" title="代码清单 3"></a>代码清单 3</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ip IP)</span> <span class="title">Mask</span><span class="params">(mask IPMask)</span> <span class="title">IP</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(mask) == IPv6len &amp;&amp; <span class="built_in">len</span>(ip) == IPv4len &amp;&amp; allFF(mask[:<span class="number">12</span>]) &#123;</span><br><span class="line">        mask = mask[<span class="number">12</span>:]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(mask) == IPv4len &amp;&amp; <span class="built_in">len</span>(ip) == IPv6len &amp;&amp; bytesEqual(ip[:<span class="number">12</span>], v4InV6Prefix) &#123;</span><br><span class="line">        ip = ip[<span class="number">12</span>:]</span><br><span class="line">    &#125;</span><br><span class="line">    n := <span class="built_in">len</span>(ip)</span><br><span class="line">    <span class="keyword">if</span> n != <span class="built_in">len</span>(mask) &#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">    out := <span class="built_in">make</span>(IP, 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">        out[i] = ip[i] &amp; mask[i]</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> out</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>请注意，此方法是一种转变操作，并使用值语义的 API 样式。它使用 IP 值作为接收方，并根据传入的 IPMask 值创建一个新的 IP 值并将其返回给调用方。该方法遵循对引用类型使用值语义（的基本指导原则）。</p><p>这跟系统默认的 append 函数有点相似。</p><h3 id="代码清单-4"><a href="#代码清单-4" class="headerlink" title="代码清单 4"></a>代码清单 4</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> data []<span class="keyword">string</span></span><br><span class="line">data = <span class="built_in">append</span>(data, <span class="string">"string"</span>)</span><br></pre></td></tr></table></figure><p>append 函数的转变操作使用值语义。将切片值传递给 append，并在变化之后返回一个新切片值。</p><p>总是除了 unmarshaling，它需要使用指针语义。</p><h3 id="代码清单-5"><a href="#代码清单-5" class="headerlink" title="代码清单 5"></a>代码清单 5</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(ip *IP)</span> <span class="title">UnmarshalText</span><span class="params">(text []<span class="keyword">byte</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> <span class="built_in">len</span>(text) == <span class="number">0</span> &#123;</span><br><span class="line">  *ip = <span class="literal">nil</span></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">  &#125;</span><br><span class="line">  s := <span class="keyword">string</span>(text)</span><br><span class="line">  x := ParseIP(s)</span><br><span class="line">  <span class="keyword">if</span> x == <span class="literal">nil</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> &amp;ParseError&#123;Type: <span class="string">"IP address"</span>, Text: s&#125;</span><br><span class="line">  &#125;</span><br><span class="line">  *ip = x</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>UnmarshalText 实现 encoding.TextUnmarshaler 接口。如果没有使用指针语义，根本无法实现。但这是可以的，因为共享值通常是安全的。除了 unmarshaling 之外，如果为一个引用类型使用指针语义，你应该三思。</p><h3 id="用户定义类型（User-Defined-Types）"><a href="#用户定义类型（User-Defined-Types）" class="headerlink" title="用户定义类型（User Defined Types）"></a>用户定义类型（User Defined Types）</h3><p>这是你最多需要作出决定的地方。你必须在你声明类型的时候决定使用什么语义。</p><p>如果我要求你给 time 包编写 API 接口，给你这种类型。</p><h3 id="代码清单-6"><a href="#代码清单-6" class="headerlink" title="代码清单 6"></a>代码清单 6</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Time <span class="keyword">struct</span> &#123;</span><br><span class="line">    sec  <span class="keyword">int64</span></span><br><span class="line">    nsec <span class="keyword">int32</span></span><br><span class="line">    loc  *Location</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你会使用什么语义？</p><p>在 Time 包中查看此类型的实现以及工厂函数 Now。</p><h3 id="代码清单-7"><a href="#代码清单-7" class="headerlink" title="代码清单 7"></a>代码清单 7</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Now</span><span class="params">()</span> <span class="title">Time</span></span> &#123;</span><br><span class="line">  sec, nsec := now()</span><br><span class="line">  <span class="keyword">return</span> Time&#123;sec + unixToInternal, nsec, Local&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工厂函数对于类型来说是一种非常重要的函数，因为它告诉你（这种类型）所选择的语义。Now 函数就很清晰地（向我们）表明使用了值语义。该函数创建一个类型为 Time 的值并将该值的副本返回给调用者。 共享 Time 值不是必要的，（因为）他们的生命周期内不需要一直存在于堆上。</p><p>再看一下 Add 方法，它也是一个转变操作。</p><h3 id="代码清单-8"><a href="#代码清单-8" class="headerlink" title="代码清单 8"></a>代码清单 8</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t Time)</span> <span class="title">Add</span><span class="params">(d Duration)</span> <span class="title">Time</span></span> &#123;</span><br><span class="line">  t.sec += <span class="keyword">int64</span>(d / <span class="number">1e9</span>)</span><br><span class="line">  nsec := t.nsec + <span class="keyword">int32</span>(d%<span class="number">1e9</span>)</span><br><span class="line">  <span class="keyword">if</span> nsec &gt;= <span class="number">1e9</span> &#123;</span><br><span class="line">  t.sec++</span><br><span class="line">  nsec -= <span class="number">1e9</span></span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> nsec &lt; <span class="number">0</span> &#123;</span><br><span class="line">  t.sec--</span><br><span class="line">  nsec += <span class="number">1e9</span></span><br><span class="line">  &#125;</span><br><span class="line">  t.nsec = nsec</span><br><span class="line">  <span class="keyword">return</span> t</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>你可以再次看到 Add 方法遵循类型所选择的语义。Add 方法使用一个值接收器来操作它自己的 Time 值副本。其中，Time 值副本在调用中使用。它将修改自己的副本，并将 Time 值的新副本返回给调用者。</p><p>以下是一个接受 Time 值的函数：</p><h3 id="代码清单-9"><a href="#代码清单-9" class="headerlink" title="代码清单 9"></a>代码清单 9</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">div</span><span class="params">(t Time, d Duration)</span> <span class="params">(qmod2 <span class="keyword">int</span>, r Duration)</span></span> &#123;</span><br></pre></td></tr></table></figure><p>再一次，接受 Time 类型的值使用值语义。唯一使用指针语义的 Time API 接口，是这些 Unmarshal 相关的函数：</p><h3 id="代码清单-10"><a href="#代码清单-10" class="headerlink" title="代码清单 10"></a>代码清单 10</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *Time)</span> <span class="title">UnmarshalBinary</span><span class="params">(data []<span class="keyword">byte</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *Time)</span> <span class="title">GobDecode</span><span class="params">(data []<span class="keyword">byte</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *Time)</span> <span class="title">UnmarshalJSON</span><span class="params">(data []<span class="keyword">byte</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *Time)</span> <span class="title">UnmarshalText</span><span class="params">(data []<span class="keyword">byte</span>)</span> <span class="title">error</span></span> &#123;</span><br></pre></td></tr></table></figure><p>大多数情况下，使用值语义的能力是有限的。将值从一个函数传递到另一个函数，（通常）使用值拷贝的方法是不正确或者不合理的。修改数据需要将其隔离成单个值再进行共享。这时，应该使用指针语义。如果你没办法 100% 确定拷贝值是正确并且合理的，那就使用指针语义吧。</p><p>查看 os 包中的 File 类型的生产函数。</p><h3 id="代码清单-11"><a href="#代码清单-11" class="headerlink" title="代码清单 11"></a>代码清单 11</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Open</span><span class="params">(name <span class="keyword">string</span>)</span> <span class="params">(file *File, err error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> OpenFile(name, O_RDONLY, <span class="number">0</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Open 函数返回一个 File 类型的指针。这意味着，对于 File 类型值，你应该使用指针语义来共享 File 的值。将指针语义修改为值语义，可能会对你的程序造成破坏性影响。当你与一个函数共享值时，最好假定你不允许拷贝值的指针并使用这个指针。否则，不知道将会出现什么样的异常情况。</p><p>查看更多的 API， 你将会看到更多使用指针语义的例子。</p><h3 id="代码清单-12"><a href="#代码清单-12" class="headerlink" title="代码清单 12"></a>代码清单 12</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(f *File)</span> <span class="title">Chdir</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> f == <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ErrInvalid</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> e := syscall.Fchdir(f.fd); e != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> &amp;PathError&#123;<span class="string">"chdir"</span>, f.name, e&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然 File 值永远不会被修改，但是 Chdir 方法还是使用指针语义。该方法必须遵循该类型的语义约定。</p><h3 id="代码清单-13"><a href="#代码清单-13" class="headerlink" title="代码清单 13"></a>代码清单 13</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">epipecheck</span><span class="params">(file *File, e error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> e == syscall.EPIPE &#123;</span><br><span class="line">        <span class="keyword">if</span> atomic.AddInt32(&amp;file.nepipe, <span class="number">1</span>) &gt;= <span class="number">10</span> &#123;</span><br><span class="line">            sigpipe()</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        atomic.StoreInt32(&amp;file.nepipe, <span class="number">0</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个名为 epipecheck 的函数，它使用指针来接收 File 值。再次注意一下，对于 File 值，一致使用指针语义。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>我在做代码 review 时，会寻找值或者指针语义是否使用一致。它可以帮助你保证代码的一致性和可预测性。它还使每个人能保持清晰和一致的心智模型。随着代码库和团队变得越来越大，值或者指针语义的一致性使用将会越来越重要。</p><p>Go 语言令人不解的地方在于指针和值语义之间的选择早已超出了接收器和函数参数的声明范围。接口的机制，函数值和切片都在语言的工作范围内。在将来的文章中，我将在这些语言的不同部分中展示值或者指针语义。</p><p>via: <a href="https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html</a></p><p>作者：<a href="https://github.com/ardanlabs/gotraining" target="_blank" rel="noopener">William Kennedy</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：</p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前序（Prelude）&quot;&gt;&lt;a href=&quot;#前序（Prelude）&quot; class=&quot;headerlink&quot; title=&quot;前序（Prelude）&quot;&gt;&lt;/a&gt;前序（Prelude）&lt;/h2&gt;&lt;p&gt;本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值或者指针传递。这是最后一篇，重点介绍在代码中使用值和指针的数据和语义的设计哲学。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言机制之内存分析</title>
    <link href="http://yoursite.com/2018/02/24/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E5%86%85%E5%AD%98%E5%88%86%E6%9E%90/"/>
    <id>http://yoursite.com/2018/02/24/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E5%86%85%E5%AD%98%E5%88%86%E6%9E%90/</id>
    <published>2018-02-24T12:54:10.000Z</published>
    <updated>2020-05-02T14:11:14.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前序（Prelude）"><a href="#前序（Prelude）" class="headerlink" title="前序（Prelude）"></a>前序（Prelude）</h2><p>本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值/指针传递。这是第三篇，主要介绍堆和逃逸分析。（译者注：这一篇可看成第二篇的进阶版）</p><a id="more"></a><p>以下是本系列文章的索引：</p><p>1) <a href="https://www.goinggo.net/2017/05/language-mechanics-on-stacks-and-pointers.html" target="_blank" rel="noopener">Go 语言机制之栈与指针</a><br>2) <a href="https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html" target="_blank" rel="noopener">Go 语言机制之逃逸分析</a><br>3) <a href="https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html" target="_blank" rel="noopener">Go 语言机制之内存剖析</a><br>4) <a href="https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html" target="_blank" rel="noopener">Go 语言机制之数据和语法的设计哲学</a></p><p>观看这段示例代码的视频演示：<a href="https://engineers.sg/video/go-concurrency-live-gophercon-sg-2017--1746" target="_blank" rel="noopener">GopherCon Singapore (2017) - Escape Analysis</a></p><h2 id="介绍（Introduction）"><a href="#介绍（Introduction）" class="headerlink" title="介绍（Introduction）"></a>介绍（Introduction）</h2><p>在前面的博文中，通过一个共享在 goroutine 的栈上的值的例子讲解了逃逸分析的基础。还有其他没有介绍的造成值逃逸的场景。为了帮助大家理解，我将调试一个分配内存的程序，并使用非常有趣的方法。</p><h2 id="程序-（The-Program）"><a href="#程序-（The-Program）" class="headerlink" title="程序 （The Program）"></a>程序 （The Program）</h2><p>我想了解 <code>io</code> 包，所以我创建了一个简单的项目。给定一个字符序列，写一个函数，可以找到字符串 <code>elvis</code> 并用大写开头的 <code>Elvis</code> 替换它。我们正在讨论国王（Elvis 即猫王，摇滚明星），他的名字总是大写的。</p><p>这是一个解决方案的链接：<br><a href="https://play.golang.org/p/n_SzF4Cer4" target="_blank" rel="noopener">https://play.golang.org/p/n_SzF4Cer4</a></p><p>这是一个压力测试的链接：<br><a href="https://play.golang.org/p/TnXrxJVfLV" target="_blank" rel="noopener">https://play.golang.org/p/TnXrxJVfLV</a></p><p>代码列表里面有两个不同的函数可以解决这个问题。这篇博文将会关注（其中的）<code>algOne</code> 函数，因为它使用到了 <code>io</code> 库。你可以自己用下 <code>algTwo</code>，体验一下内存，CPU 消耗的差异。</p><h3 id="清单-1"><a href="#清单-1" class="headerlink" title="清单 1"></a>清单 1</h3><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">Input:</span><br><span class="line">abcelvisaElvisabcelviseelvisaelvisaabeeeelvise l v i saa bb e l v i saa elvi</span><br><span class="line">selvielviselvielvielviselvi1elvielviselvis</span><br><span class="line"></span><br><span class="line">Output:</span><br><span class="line">abcElvisaElvisabcElviseElvisaElvisaabeeeElvise l v i saa bb e l v i saa elvi</span><br><span class="line">selviElviselvielviElviselvi1elviElvisElvis</span><br></pre></td></tr></table></figure><p>这是完整的 <code>algOne</code> 函数</p><h3 id="清单-2"><a href="#清单-2" class="headerlink" title="清单 2"></a>清单 2</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">algOne</span><span class="params">(data []<span class="keyword">byte</span>, find []<span class="keyword">byte</span>, repl []<span class="keyword">byte</span>, output *bytes.Buffer)</span></span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Use a bytes Buffer to provide a stream to process.</span></span><br><span class="line">    input := bytes.NewBuffer(data)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// The number of bytes we are looking for.</span></span><br><span class="line">    size := <span class="built_in">len</span>(find)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Declare the buffers we need to process the stream.</span></span><br><span class="line">    buf := <span class="built_in">make</span>([]<span class="keyword">byte</span>, size)</span><br><span class="line">    end := size - <span class="number">1</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Read in an initial number of bytes we need to get started.</span></span><br><span class="line">    <span class="keyword">if</span> n, err := io.ReadFull(input, buf[:end]); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        output.Write(buf[:n])</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">for</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Read in one byte from the input stream.</span></span><br><span class="line">        <span class="keyword">if</span> _, err := io.ReadFull(input, buf[end:]); err != <span class="literal">nil</span> &#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// Flush the reset of the bytes we have.</span></span><br><span class="line">            output.Write(buf[:end])</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// If we have a match, replace the bytes.</span></span><br><span class="line">        <span class="keyword">if</span> bytes.Compare(buf, find) == <span class="number">0</span> &#123;</span><br><span class="line">            output.Write(repl)</span><br><span class="line"></span><br><span class="line">            <span class="comment">// Read a new initial number of bytes.</span></span><br><span class="line">            <span class="keyword">if</span> n, err := io.ReadFull(input, buf[:end]); err != <span class="literal">nil</span> &#123;</span><br><span class="line">                output.Write(buf[:n])</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">continue</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Write the front byte since it has been compared.</span></span><br><span class="line">        output.WriteByte(buf[<span class="number">0</span>])</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Slice that front byte out.</span></span><br><span class="line">        <span class="built_in">copy</span>(buf, buf[<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>我想知道的是这个函数的性能表现得怎么样，以及它在堆上分配带来什么样的压力。为了这个目的，我们将进行压力测试。</p><h2 id="压力测试（Benchmarking）"><a href="#压力测试（Benchmarking）" class="headerlink" title="压力测试（Benchmarking）"></a>压力测试（Benchmarking）</h2><p>这个是我写的压力测试函数，它在内部调用 <code>algOne</code> 函数去处理数据流。</p><h3 id="清单-3"><a href="#清单-3" class="headerlink" title="清单 3"></a>清单 3</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">BenchmarkAlgorithmOne</span><span class="params">(b *testing.B)</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> output bytes.Buffer</span><br><span class="line">    in := assembleInputStream()</span><br><span class="line">    find := []<span class="keyword">byte</span>(<span class="string">"elvis"</span>)</span><br><span class="line">    repl := []<span class="keyword">byte</span>(<span class="string">"Elvis"</span>)</span><br><span class="line"></span><br><span class="line">    b.ResetTimer()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; b.N; i++ &#123;</span><br><span class="line">        output.Reset()</span><br><span class="line">        algOne(in, find, repl, &amp;output)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有这个压力测试函数，我们就可以运行 <code>go test</code> 并使用 -<code>bench</code>，-<code>benchtime</code> 和 -<code>benchmem</code> 选项。</p><h3 id="清单-4"><a href="#清单-4" class="headerlink" title="清单 4"></a>清单 4</h3><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">$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem</span><br><span class="line">BenchmarkAlgorithmOne-8    2000000      2522 ns&#x2F;op       117 B&#x2F;op        2 allocs&#x2F;op</span><br></pre></td></tr></table></figure><p>运行完压力测试后，我们可以看到 <code>algOne</code> 函数分配了两次值，每次分配了 117 个字节。这真的很棒，但我们还需要知道哪行代码造成了分配。为了这个目的，我们需要生成压力测试的分析数据。</p><h2 id="性能分析（Profiling）"><a href="#性能分析（Profiling）" class="headerlink" title="性能分析（Profiling）"></a>性能分析（Profiling）</h2><p>为了生成分析数据，我们将再次运行压力测试，但这次为了生成内存检测数据，我们打开 -<code>memprofile</code> 开关。</p><h3 id="清单-5"><a href="#清单-5" class="headerlink" title="清单 5"></a>清单 5</h3><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">$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out</span><br><span class="line">BenchmarkAlgorithmOne-8    2000000      2570 ns&#x2F;op       117 B&#x2F;op        2 allocs&#x2F;op</span><br></pre></td></tr></table></figure><p>一旦压力测试完成，测试工具就会生成两个新的文件。</p><h3 id="清单-6"><a href="#清单-6" class="headerlink" title="清单 6"></a>清单 6</h3><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">~&#x2F;code&#x2F;go&#x2F;src&#x2F;...&#x2F;memcpu</span><br><span class="line">$ ls - &#39;</span><br><span class="line">total 9248</span><br><span class="line">-rw-r--r--  1 bill  staff      209 May 22 18:11 mem.out       (NEW)</span><br><span class="line">-rwxr-xr-x  1 bill  staff  2847600 May 22 18:10 memcpu.test   (NEW)</span><br><span class="line">-rw-r--r--  1 bill  staff     4761 May 22 18:01 stream.go</span><br><span class="line">-rw-r--r--  1 bill  staff      880 May 22 14:49 stream_test.go</span><br></pre></td></tr></table></figure><p>源码在 <code>memcpu</code> 目录中，<code>algOne</code> 函数在 <code>stream.go</code> 文件中，压力测试函数在 <code>stream_test.go</code> 文件中。新生成的文件为 <code>mem.out</code> 和 <code>memcpu.test</code>。<code>mem.out</code> 包含分析数据和 <code>memcpu.test</code> 文件，以及包含我们查看分析数据时需要访问符号的二进制文件。</p><p>有了分析数据和二进制测试文件，我们就可以运行 <code>pprof</code> 工具学习数据分析。</p><h3 id="清单-7"><a href="#清单-7" class="headerlink" title="清单 7"></a>清单 7</h3><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></pre></td><td class="code"><pre><span class="line">$ go tool pprof -alloc_space memcpu.test mem.out</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands)</span><br><span class="line">(pprof) _</span><br></pre></td></tr></table></figure><p>当分析内存数据时，为了轻而易举地得到我们要的信息，你会想用 -<code>alloc_space</code> 选项替代默认的 -<code>inuse_space</code> 选项。这将会向你展示每一次分配发生在哪里，不管你分析数据时它是不是还在内存中。</p><p>在 <code>（pprof）</code> 提示下，我们使用 <code>list</code> 命令检查 <code>algOne</code> 函数。这个命令可以使用正则表达式作为参数找到你要的函数。</p><h3 id="清单-8"><a href="#清单-8" class="headerlink" title="清单 8"></a>清单 8</h3><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><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">(pprof) list algOne</span><br><span class="line">Total: 335.03MB</span><br><span class="line">ROUTINE &#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; ...&#x2F;memcpu.algOne in code&#x2F;go&#x2F;src&#x2F;...&#x2F;memcpu&#x2F;stream.go</span><br><span class="line"> 335.03MB   335.03MB (flat, cum)   100% of Total</span><br><span class="line">        .          .     78:</span><br><span class="line">        .          .     79:&#x2F;&#x2F; algOne is one way to solve the problem.</span><br><span class="line">        .          .     80:func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) &#123;</span><br><span class="line">        .          .     81:</span><br><span class="line">        .          .     82: &#x2F;&#x2F; Use a bytes Buffer to provide a stream to process.</span><br><span class="line"> 318.53MB   318.53MB     83: input :&#x3D; bytes.NewBuffer(data)</span><br><span class="line">        .          .     84:</span><br><span class="line">        .          .     85: &#x2F;&#x2F; The number of bytes we are looking for.</span><br><span class="line">        .          .     86: size :&#x3D; len(find)</span><br><span class="line">        .          .     87:</span><br><span class="line">        .          .     88: &#x2F;&#x2F; Declare the buffers we need to process the stream.</span><br><span class="line">  16.50MB    16.50MB     89: buf :&#x3D; make([]byte, size)</span><br><span class="line">        .          .     90: end :&#x3D; size - 1</span><br><span class="line">        .          .     91:</span><br><span class="line">        .          .     92: &#x2F;&#x2F; Read in an initial number of bytes we need to get started.</span><br><span class="line">        .          .     93: if n, err :&#x3D; io.ReadFull(input, buf[:end]); err !&#x3D; nil || n &lt; end &#123;</span><br><span class="line">        .          .     94:       output.Write(buf[:n])</span><br><span class="line">(pprof) _</span><br></pre></td></tr></table></figure><p>基于这次的数据分析，我们现在知道了 <code>input</code>，<code>buf</code> 数组在堆中分配。因为 <code>input</code> 是指针变量，分析数据表明 <code>input</code> 指针变量指定的 <code>bytes.Buffer</code> 值分配了。我们先关注 <code>input</code> 内存分配以及弄清楚为啥会被分配。</p><p>我们可以假定它被分配是因为调用 <code>bytes.NewBuffer</code> 函数时在栈上共享了 <code>bytes.Buffer</code> 值。然而，存在于 <code>flat</code> 列（pprof 输出的第一列）的值告诉我们值被分配是因为 <code>algOne</code> 函数共享造成了它的逃逸。</p><p>我知道 <code>flat</code> 列代表在函数中的分配是因为 <code>list</code> 命令显示 <code>Benchmark</code> 函数中调用了 <code>aglOne</code>。</p><h3 id="清单-9"><a href="#清单-9" class="headerlink" title="清单 9"></a>清单 9</h3><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">(pprof) list Benchmark</span><br><span class="line">Total: 335.03MB</span><br><span class="line">ROUTINE &#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; ...&#x2F;memcpu.BenchmarkAlgorithmOne in code&#x2F;go&#x2F;src&#x2F;...&#x2F;memcpu&#x2F;stream_test.go</span><br><span class="line">        0   335.03MB (flat, cum)   100% of Total</span><br><span class="line">        .          .     18: find :&#x3D; []byte(&quot;elvis&quot;)</span><br><span class="line">        .          .     19: repl :&#x3D; []byte(&quot;Elvis&quot;)</span><br><span class="line">        .          .     20:</span><br><span class="line">        .          .     21: b.ResetTimer()</span><br><span class="line">        .          .     22:</span><br><span class="line">        .   335.03MB     23: for i :&#x3D; 0; i &lt; b.N; i++ &#123;</span><br><span class="line">        .          .     24:       output.Reset()</span><br><span class="line">        .          .     25:       algOne(in, find, repl, &amp;output)</span><br><span class="line">        .          .     26: &#125;</span><br><span class="line">        .          .     27:&#125;</span><br><span class="line">        .          .     28:</span><br><span class="line">(pprof) _</span><br></pre></td></tr></table></figure><p>因为在 <code>cum</code> 列（第二列）只有一个值，这告诉我 <code>Benchmark</code> 没有直接分配。所有的内存分配都发生在函数调用的循环里。你可以看到这两个 <code>list</code> 调用的分配次数是匹配的。</p><p>我们还是不知道为什么 <code>bytes.Buffer</code> 值被分配。这时在 <code>go build</code> 的时候打开 -<code>gcflags &quot;-m -m&quot;</code> 就派上用场了。分析数据只能告诉你哪些值逃逸，但编译命令可以告诉你为啥。</p><h2 id="编译器报告（Compiler-Reporting）"><a href="#编译器报告（Compiler-Reporting）" class="headerlink" title="编译器报告（Compiler Reporting）"></a>编译器报告（Compiler Reporting）</h2><p>让我们看一下编译器关于代码中逃逸分析的判决。</p><h3 id="清单-10"><a href="#清单-10" class="headerlink" title="清单 10"></a>清单 10</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -gcflags <span class="string">"-m -m"</span></span><br></pre></td></tr></table></figure><p>这个命令产生了一大堆的输出。我们只需要搜索输出中包含 <code>stream.go:83</code>，因为 <code>stream.go</code> 是包含这段代码的文件名并且第 83 行包含 <code>bytes.Buffer</code> 的值。搜索后我们找到 6 行。</p><h3 id="清单-11"><a href="#清单-11" class="headerlink" title="清单 11"></a>清单 11</h3><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">.&#x2F;stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer &#123; return &amp;bytes.Buffer literal &#125;</span><br><span class="line"></span><br><span class="line">.&#x2F;stream.go:83: &amp;bytes.Buffer literal escapes to heap</span><br><span class="line">.&#x2F;stream.go:83:   from ~r0 (assign-pair) at .&#x2F;stream.go:83</span><br><span class="line">.&#x2F;stream.go:83:   from input (assigned) at .&#x2F;stream.go:83</span><br><span class="line">.&#x2F;stream.go:83:   from input (interface-converted) at .&#x2F;stream.go:93</span><br><span class="line">.&#x2F;stream.go:83:   from input (passed to call[argument escapes]) at .&#x2F;stream.go:93</span><br></pre></td></tr></table></figure><p>我们搜索 <code>stream.go:83</code> 找到的第一行很有趣。</p><h3 id="清单-12"><a href="#清单-12" class="headerlink" title="清单 12"></a>清单 12</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.&#x2F;stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer &#123; return &amp;bytes.Buffer literal &#125;</span><br></pre></td></tr></table></figure><p>可以肯定 <code>bytes.Buffer</code> 值没有逃逸，因为它传递给了调用栈。这是因为没有调用 <code>bytes.NewBuffer</code>，函数内联处理了。</p><p>所以这是我写的代码片段：</p><h2 id="清单-13"><a href="#清单-13" class="headerlink" title="清单 13"></a>清单 13</h2> <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">83</span>     input := bytes.NewBuffer(data)</span><br></pre></td></tr></table></figure><p>因为编译器选择内联 <code>bytes.NewBuffer</code> 函数调用，我写的代码被转成：</p><h3 id="清单-14"><a href="#清单-14" class="headerlink" title="清单 14"></a>清单 14</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">input := &amp;bytes.Buffer&#123;buf: data&#125;</span><br></pre></td></tr></table></figure><p>这意味着 <code>algOne</code> 函数直接构造 <code>bytes.Buffer</code> 值。那么，现在的问题是什么造成了值从 <code>algOne</code> 栈帧中逃逸？答案在我们搜索结果中的另外 5 行。</p><h3 id="清单-15"><a href="#清单-15" class="headerlink" title="清单 15"></a>清单 15</h3><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></pre></td><td class="code"><pre><span class="line">.&#x2F;stream.go:83: &amp;bytes.Buffer literal escapes to heap</span><br><span class="line">.&#x2F;stream.go:83:   from ~r0 (assign-pair) at .&#x2F;stream.go:83</span><br><span class="line">.&#x2F;stream.go:83:   from input (assigned) at .&#x2F;stream.go:83</span><br><span class="line">.&#x2F;stream.go:83:   from input (interface-converted) at .&#x2F;stream.go:93</span><br><span class="line">.&#x2F;stream.go:83:   from input (passed to call[argument escapes]) at .&#x2F;stream.go:93</span><br></pre></td></tr></table></figure><p>这几行告诉我们代码中的第 93 行造成了逃逸。<code>input</code> 变量被赋值给一个接口变量。</p><h2 id="接口（Interfaces）"><a href="#接口（Interfaces）" class="headerlink" title="接口（Interfaces）"></a>接口（Interfaces）</h2><p>我完全不记得在代码中将值赋给了接口变量。然而，如果你看到 93 行，就可以非常清楚地看到发生了什么。</p><h3 id="清单-16"><a href="#清单-16" class="headerlink" title="清单 16"></a>清单 16</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">93</span>     <span class="keyword">if</span> n, err := io.ReadFull(input, buf[:end]); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="number">94</span>         output.Write(buf[:n])</span><br><span class="line"><span class="number">95</span>         <span class="keyword">return</span></span><br><span class="line"><span class="number">96</span>     &#125;</span><br></pre></td></tr></table></figure><p><code>io.ReadFull</code> 调用造成了接口赋值。如果你看了 <code>io.ReadFull</code> 函数的定义，你可以看到一个接口类型是如何接收 <code>input</code> 值。</p><h3 id="清单-17"><a href="#清单-17" class="headerlink" title="清单 17"></a>清单 17</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Reader <span class="keyword">interface</span> &#123;</span><br><span class="line">      Read(p []<span class="keyword">byte</span>) (n <span class="keyword">int</span>, 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">ReadFull</span><span class="params">(r Reader, buf []<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">return</span> ReadAtLeast(r, buf, <span class="built_in">len</span>(buf))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>传递 <code>bytes.Buffer</code> 地址到调用栈，在 <code>Reader</code> 接口变量中存储会造成一次逃逸。现在我们知道使用接口变量是需要开销的：分配和重定向。所以，如果没有很明显的使用接口的原因，你可能不想使用接口。下面是我选择在我的代码中是否使用接口的原则。</p><p>使用接口的情况：</p><ul><li>用户 API 需要提供实现细节的时候。</li><li>API 的内部需要维护多种实现。</li><li>可以改变的 API 部分已经被识别并需要解耦。</li></ul><p>不使用接口的情况：</p><ul><li>为了使用接口而使用接口。</li><li>推广算法。</li><li>当用户可以定义自己的接口时。</li></ul><p>现在我们可以问自己，这个算法真的需要 <code>io.ReadFull 函数吗？答案是否定的，因为</code>bytes.Buffer` 类型有一个方法可以供我们使用。使用方法而不是调用一个函数可以防止重新分配内存。</p><p>让我们修改代码，删除 <code>io</code> 包，并直接使用 <code>Read</code> 函数而不是 <code>input</code> 变量。</p><p>修改后的代码删除了 <code>io</code> 包的调用，为了保留相同的行号，我使用空标志符替代 <code>io</code> 包的引用。这会允许（没有使用的）库导入的行待在列表中。</p><h3 id="清单-18"><a href="#清单-18" class="headerlink" title="清单 18"></a>清单 18</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"> <span class="number">12</span> <span class="keyword">import</span> (</span><br><span class="line"> <span class="number">13</span>     <span class="string">"bytes"</span></span><br><span class="line"> <span class="number">14</span>     <span class="string">"fmt"</span></span><br><span class="line"> <span class="number">15</span>     _ <span class="string">"io"</span></span><br><span class="line"> <span class="number">16</span> )</span><br><span class="line"></span><br><span class="line"> <span class="number">80</span> <span class="function"><span class="keyword">func</span> <span class="title">algOne</span><span class="params">(data []<span class="keyword">byte</span>, find []<span class="keyword">byte</span>, repl []<span class="keyword">byte</span>, output *bytes.Buffer)</span></span> &#123;</span><br><span class="line"> <span class="number">81</span></span><br><span class="line"> <span class="number">82</span>     <span class="comment">// Use a bytes Buffer to provide a stream to process.</span></span><br><span class="line"> <span class="number">83</span>     input := bytes.NewBuffer(data)</span><br><span class="line"> <span class="number">84</span></span><br><span class="line"> <span class="number">85</span>     <span class="comment">// The number of bytes we are looking for.</span></span><br><span class="line"> <span class="number">86</span>     size := <span class="built_in">len</span>(find)</span><br><span class="line"> <span class="number">87</span></span><br><span class="line"> <span class="number">88</span>     <span class="comment">// Declare the buffers we need to process the stream.</span></span><br><span class="line"> <span class="number">89</span>     buf := <span class="built_in">make</span>([]<span class="keyword">byte</span>, size)</span><br><span class="line"> <span class="number">90</span>     end := size - <span class="number">1</span></span><br><span class="line"> <span class="number">91</span></span><br><span class="line"> <span class="number">92</span>     <span class="comment">// Read in an initial number of bytes we need to get started.</span></span><br><span class="line"> <span class="number">93</span>     <span class="keyword">if</span> n, err := input.Read(buf[:end]); err != <span class="literal">nil</span> || n &lt; end &#123;</span><br><span class="line"> <span class="number">94</span>         output.Write(buf[:n])</span><br><span class="line"> <span class="number">95</span>         <span class="keyword">return</span></span><br><span class="line"> <span class="number">96</span>     &#125;</span><br><span class="line"> <span class="number">97</span></span><br><span class="line"> <span class="number">98</span>     <span class="keyword">for</span> &#123;</span><br><span class="line"> <span class="number">99</span></span><br><span class="line"><span class="number">100</span>         <span class="comment">// Read in one byte from the input stream.</span></span><br><span class="line"><span class="number">101</span>         <span class="keyword">if</span> _, err := input.Read(buf[end:]); err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="number">102</span></span><br><span class="line"><span class="number">103</span>             <span class="comment">// Flush the reset of the bytes we have.</span></span><br><span class="line"><span class="number">104</span>             output.Write(buf[:end])</span><br><span class="line"><span class="number">105</span>             <span class="keyword">return</span></span><br><span class="line"><span class="number">106</span>         &#125;</span><br><span class="line"><span class="number">107</span></span><br><span class="line"><span class="number">108</span>         <span class="comment">// If we have a match, replace the bytes.</span></span><br><span class="line"><span class="number">109</span>         <span class="keyword">if</span> bytes.Compare(buf, find) == <span class="number">0</span> &#123;</span><br><span class="line"><span class="number">110</span>             output.Write(repl)</span><br><span class="line"><span class="number">111</span></span><br><span class="line"><span class="number">112</span>             <span class="comment">// Read a new initial number of bytes.</span></span><br><span class="line"><span class="number">113</span>             <span class="keyword">if</span> n, err := input.Read(buf[:end]); err != <span class="literal">nil</span> || n &lt; end &#123;</span><br><span class="line"><span class="number">114</span>                 output.Write(buf[:n])</span><br><span class="line"><span class="number">115</span>                 <span class="keyword">return</span></span><br><span class="line"><span class="number">116</span>             &#125;</span><br><span class="line"><span class="number">117</span></span><br><span class="line"><span class="number">118</span>             <span class="keyword">continue</span></span><br><span class="line"><span class="number">119</span>         &#125;</span><br><span class="line"><span class="number">120</span></span><br><span class="line"><span class="number">121</span>         <span class="comment">// Write the front byte since it has been compared.</span></span><br><span class="line"><span class="number">122</span>         output.WriteByte(buf[<span class="number">0</span>])</span><br><span class="line"><span class="number">123</span></span><br><span class="line"><span class="number">124</span>         <span class="comment">// Slice that front byte out.</span></span><br><span class="line"><span class="number">125</span>         <span class="built_in">copy</span>(buf, buf[<span class="number">1</span>:])</span><br><span class="line"><span class="number">126</span>     &#125;</span><br><span class="line"><span class="number">127</span> &#125;</span><br></pre></td></tr></table></figure><p>修改后我们执行压力测试，可以看到 <code>bytes.Buffer</code> 的分配消失了。</p><h3 id="清单-19"><a href="#清单-19" class="headerlink" title="清单 19"></a>清单 19</h3><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">$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out</span><br><span class="line">BenchmarkAlgorithmOne-8    2000000      1814 ns&#x2F;op         5 B&#x2F;op        1 allocs&#x2F;op</span><br></pre></td></tr></table></figure><p>我们可以看到大约 29% 的性能提升。代码从 <code>2570 ns/op</code> 降到 <code>1814 ns/op</code>。解决了这个问题，我们现在可以关注 <code>buf</code> 切片数组。如果再次使用测试代码生成分析数据，我们应该能够识别到造成剩下的分配的原因。</p><h2 id="清单-20"><a href="#清单-20" class="headerlink" title="清单 20"></a>清单 20</h2><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><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">$ go tool pprof -alloc_space memcpu.test mem.out</span><br><span class="line">Entering interactive mode (type &quot;help&quot; for commands)</span><br><span class="line">(pprof) list algOne</span><br><span class="line">Total: 7.50MB</span><br><span class="line">ROUTINE &#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D;&#x3D; ...&#x2F;memcpu.BenchmarkAlgorithmOne in code&#x2F;go&#x2F;src&#x2F;...&#x2F;memcpu&#x2F;stream_test.go</span><br><span class="line">     11MB       11MB (flat, cum)   100% of Total</span><br><span class="line">        .          .     84:</span><br><span class="line">        .          .     85: &#x2F;&#x2F; The number of bytes we are looking for.</span><br><span class="line">        .          .     86: size :&#x3D; len(find)</span><br><span class="line">        .          .     87:</span><br><span class="line">        .          .     88: &#x2F;&#x2F; Declare the buffers we need to process the stream.</span><br><span class="line">     11MB       11MB     89: buf :&#x3D; make([]byte, size)</span><br><span class="line">        .          .     90: end :&#x3D; size - 1</span><br><span class="line">        .          .     91:</span><br><span class="line">        .          .     92: &#x2F;&#x2F; Read in an initial number of bytes we need to get started.</span><br><span class="line">        .          .     93: if n, err :&#x3D; input.Read(buf[:end]); err !&#x3D; nil || n &lt; end &#123;</span><br><span class="line">        .          .     94:       output.Write(buf[:n])</span><br></pre></td></tr></table></figure><p>只剩下 89 行所示，对数组切片的分配</p><h2 id="栈帧"><a href="#栈帧" class="headerlink" title="栈帧"></a>栈帧</h2><p>想知道造成 <code>buf</code> 数组切片的分配的原因？让我们再次运行 <code>go build</code>，并使用 -<code>gcflags &quot;-m -m&quot;选项并搜索</code>stream.go:89`。</p><h2 id="清单-21"><a href="#清单-21" class="headerlink" title="清单 21"></a>清单 21</h2><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></pre></td><td class="code"><pre><span class="line">$ go build -gcflags &quot;-m -m&quot;</span><br><span class="line">.&#x2F;stream.go:89: make([]byte, size) escapes to heap</span><br><span class="line">.&#x2F;stream.go:89:   from make([]byte, size) (too large for stack) at .&#x2F;stream.go:89</span><br></pre></td></tr></table></figure><p>报告显示，对于栈来说，数组太大了。这个信息误导了我们。并不是说底层的数组太大，而是编译器在编译时并不知道数组的大小。</p><p>值只有在编译器编译时知道其大小才会将它分配到栈中。这是因为每个函数的栈帧大小是在编译时计算的。如果编译器不知道其大小，就只会在堆中分配。</p><p>为了验证（我们的想法），我们将值硬编码为 5，然后再次运行压力测试。</p><h3 id="清单-22"><a href="#清单-22" class="headerlink" title="清单 22"></a>清单 22</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">89</span>     buf := <span class="built_in">make</span>([]<span class="keyword">byte</span>, <span class="number">5</span>)</span><br></pre></td></tr></table></figure><p>这一次我们运行压力测试，分配消失了。</p><h3 id="清单-23"><a href="#清单-23" class="headerlink" title="清单 23"></a>清单 23</h3><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">$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem</span><br><span class="line">BenchmarkAlgorithmOne-8    3000000      1720 ns&#x2F;op         0 B&#x2F;op        0 allocs&#x2F;op</span><br></pre></td></tr></table></figure><p>如果你再看一下编译器报告，你会发现没有需要逃逸处理的。</p><h3 id="清单-24"><a href="#清单-24" class="headerlink" title="清单 24"></a>清单 24</h3><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></pre></td><td class="code"><pre><span class="line">$ go build -gcflags &quot;-m -m&quot;</span><br><span class="line">.&#x2F;stream.go:83: algOne &amp;bytes.Buffer literal does not escape</span><br><span class="line">.&#x2F;stream.go:89: algOne make([]byte, 5) does not escape</span><br></pre></td></tr></table></figure><p>很明显我们无法确定切片的大小，所以我们在算法中需要一次分配。</p><h2 id="分配和性能（Allocation-and-Performance）"><a href="#分配和性能（Allocation-and-Performance）" class="headerlink" title="分配和性能（Allocation and Performance）"></a>分配和性能（Allocation and Performance）</h2><p>比较一下我们在重构过程中，每次提升的性能。</p><h3 id="清单-25"><a href="#清单-25" class="headerlink" title="清单 25"></a>清单 25</h3><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">Before any optimization</span><br><span class="line">BenchmarkAlgorithmOne-8    2000000      2570 ns&#x2F;op       117 B&#x2F;op        2 allocs&#x2F;op</span><br><span class="line"></span><br><span class="line">Removing the bytes.Buffer allocation</span><br><span class="line">BenchmarkAlgorithmOne-8    2000000      1814 ns&#x2F;op         5 B&#x2F;op        1 allocs&#x2F;op</span><br><span class="line"></span><br><span class="line">Removing the backing array allocation</span><br><span class="line">BenchmarkAlgorithmOne-8    3000000      1720 ns&#x2F;op         0 B&#x2F;op        0 allocs&#x2F;op</span><br></pre></td></tr></table></figure><p>删除掉 bytes.Buffer 里面的（重新）内存分配，我们获得了大约 29% 的性能提升，删除掉所有的分配，我们能获得大约 33% 的性能提升。内存分配是应用程序性能影响因素之一。</p><h2 id="结论（Conclusion）"><a href="#结论（Conclusion）" class="headerlink" title="结论（Conclusion）"></a>结论（Conclusion）</h2><p>Go 拥有一些神奇的工具使你能了解编译器作出的跟逃逸分析相关的一些决定。基于这些信息，你可以通过重构代码使得值存在于栈中而不需要在（被重新分配到）堆中。你不是想去掉所有软件中所有的内存（再）分配，而是想最小化这些分配。</p><p>这就是说，写程序时永远不要把性能作为第一优先级，因为你并不想（在写程序时）一直猜测性能。写正确的代码才是你第一优先级。这意味着，我们首先要关注的是完整性、可读性和简单性。一旦有了可以运行的程序，才需要确定程序是否足够快。假如程序不够快，那么使用语言提供的工具来查找和解决性能问题。</p><p>via: <a href="https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html</a></p><p>作者：<a href="https://github.com/ardanlabs/gotraining" target="_blank" rel="noopener">William Kennedy</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前序（Prelude）&quot;&gt;&lt;a href=&quot;#前序（Prelude）&quot; class=&quot;headerlink&quot; title=&quot;前序（Prelude）&quot;&gt;&lt;/a&gt;前序（Prelude）&lt;/h2&gt;&lt;p&gt;本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值/指针传递。这是第三篇，主要介绍堆和逃逸分析。（译者注：这一篇可看成第二篇的进阶版）&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言机制之逃逸分析</title>
    <link href="http://yoursite.com/2018/02/12/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90/"/>
    <id>http://yoursite.com/2018/02/12/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E6%9C%BA%E5%88%B6%E4%B9%8B%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90/</id>
    <published>2018-02-12T12:51:06.000Z</published>
    <updated>2020-05-02T14:11:03.583Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前序（Prelude）"><a href="#前序（Prelude）" class="headerlink" title="前序（Prelude）"></a>前序（Prelude）</h2><p>本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值/指针传递。这是第二篇，主要介绍堆和逃逸分析。</p><a id="more"></a><p>以下是本系列文章的索引：</p><p>1) <a href="https://www.goinggo.net/2017/05/language-mechanics-on-stacks-and-pointers.html" target="_blank" rel="noopener">Go 语言机制之栈与指针</a><br>2) <a href="https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html" target="_blank" rel="noopener">Go 语言机制之逃逸分析</a><br>3) <a href="https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html" target="_blank" rel="noopener">Go 语言机制之内存剖析</a><br>4) <a href="https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html" target="_blank" rel="noopener">Go 语言机制之数据和语法的设计哲学</a></p><h2 id="介绍（Introduction）"><a href="#介绍（Introduction）" class="headerlink" title="介绍（Introduction）"></a>介绍（Introduction）</h2><p>在四部分系列的第一部分，我用一个将值共享给 goroutine 栈的例子介绍了指针结构的基础。而我没有说的是值存在栈之上的情况。为了理解这个，你需要学习值存储的另外一个位置：堆。有这个基础，就可以开始学习逃逸分析。</p><p>逃逸分析是编译器用来决定你的程序中值的位置的过程。特别地，编译器执行静态代码分析，以确定一个构造体的实例化值是否会逃逸到堆。在 Go 语言中，你没有可用的关键字或者函数，能够直接让编译器做这个决定。只能够通过你写代码的方式来作出这个决定。</p><h2 id="堆（Heaps）"><a href="#堆（Heaps）" class="headerlink" title="堆（Heaps）"></a>堆（Heaps）</h2><p>堆是内存的第二区域，除了栈之外，用来存储值的地方。堆无法像栈一样能自清理，所以使用这部分内存会造成很大的开销（相比于使用栈）。重要的是，开销跟 GC（垃圾收集），即被牵扯进来保证这部分区域干净的程序，有很大的关系。当垃圾收集程序运行时，它会占用你的可用 CPU 容量的 25%。更有甚者，它会造成微秒级的 “stop the world” 的延时。拥有 GC 的好处是你可以不再关注堆内存的管理，这部分很复杂，是历史上容易出错的地方。</p><p>在 Go 中，会将一部分值分配到堆上。这些分配给 GC 带来了压力，因为堆上没有被指针索引的值都需要被删除。越多需要被检查和删除的值，会给每次运行 GC 时带来越多的工作。所以，分配算法不断地工作，以平衡堆的大小和它运行的速度。</p><h2 id="共享栈（Sharing-Stacks）"><a href="#共享栈（Sharing-Stacks）" class="headerlink" title="共享栈（Sharing Stacks）"></a>共享栈（Sharing Stacks）</h2><p>在 Go 语言中，不允许 goroutine 中的指针指向另外一个 goroutine 的栈。这是因为当栈增长或者收缩时，goroutine 中的栈内存会被一块新的内存替换。如果运行时需要追踪指针指向其他的 goroutine 的栈，就会造成非常多需要管理的内存，以至于更新指向那些栈的指针将使 “stop the world” 问题更严重。</p><p>这里有一个栈被替换好几次的例子。看输出的第 2 和第 6 行。你会看到 main 函数中的栈的字符串地址值改变了两次。<br><a href="https://play.golang.org/p/pxn5u4EBSI" target="_blank" rel="noopener">https://play.golang.org/p/pxn5u4EBSI</a></p><h2 id="逃逸机制（Escape-Mechanics）"><a href="#逃逸机制（Escape-Mechanics）" class="headerlink" title="逃逸机制（Escape Mechanics）"></a>逃逸机制（Escape Mechanics）</h2><p>任何时候，一个值被分享到函数栈帧范围之外，它都会在堆上被重新分配。这是逃逸分析算法发现这些情况和管控这一层的工作。（内存的）完整性在于确保对任何值的访问始终是准确、一致和高效的。</p><p>通过查看这个语言机制了解逃逸分析。<br><a href="https://play.golang.org/p/Y_VZxYteKO" target="_blank" rel="noopener">https://play.golang.org/p/Y_VZxYteKO</a></p><h3 id="表-1"><a href="#表-1" class="headerlink" title="表 1"></a>表 1</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">01</span> <span class="keyword">package</span> main</span><br><span class="line"><span class="number">02</span></span><br><span class="line"><span class="number">03</span> <span class="keyword">type</span> user <span class="keyword">struct</span> &#123;</span><br><span class="line"><span class="number">04</span>     name  <span class="keyword">string</span></span><br><span class="line"><span class="number">05</span>     email <span class="keyword">string</span></span><br><span class="line"><span class="number">06</span> &#125;</span><br><span class="line"><span class="number">07</span></span><br><span class="line"><span class="number">08</span> <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="number">09</span>     u1 := createUserV1()</span><br><span class="line"><span class="number">10</span>     u2 := createUserV2()</span><br><span class="line"><span class="number">11</span></span><br><span class="line"><span class="number">12</span>     <span class="built_in">println</span>(<span class="string">"u1"</span>, &amp;u1, <span class="string">"u2"</span>, &amp;u2)</span><br><span class="line"><span class="number">13</span> &#125;</span><br><span class="line"><span class="number">14</span></span><br><span class="line"><span class="number">15</span> <span class="comment">//go:noinline</span></span><br><span class="line"><span class="number">16</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV1</span><span class="params">()</span> <span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">17</span>     u := user&#123;</span><br><span class="line"><span class="number">18</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">19</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">20</span>     &#125;</span><br><span class="line"><span class="number">21</span></span><br><span class="line"><span class="number">22</span>     <span class="built_in">println</span>(<span class="string">"V1"</span>, &amp;u)</span><br><span class="line"><span class="number">23</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">24</span> &#125;</span><br><span class="line"><span class="number">25</span></span><br><span class="line"><span class="number">26</span> <span class="comment">//go:noinline</span></span><br><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, &amp;u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> &amp;u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>我使用 <strong><em>go:noinline</em></strong> 指令，阻止在 <strong><em>main</em></strong> 函数中，编译器使用内联代码替代函数调用。内联（优化）会使函数调用消失，并使例子复杂化。我将在下一篇博文介绍内联造成的副作用。</p><p>在表 1 中，你可以看到创建 <strong><em>user</em></strong> 值，并返回给调用者的两个不同的函数。在函数版本 1 中，返回值。</p><h3 id="表-2"><a href="#表-2" class="headerlink" title="表 2"></a>表 2</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">16</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV1</span><span class="params">()</span> <span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">17</span>     u := user&#123;</span><br><span class="line"><span class="number">18</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">19</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">20</span>     &#125;</span><br><span class="line"><span class="number">21</span></span><br><span class="line"><span class="number">22</span>     <span class="built_in">println</span>(<span class="string">"V1"</span>, &amp;u)</span><br><span class="line"><span class="number">23</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">24</span> &#125;</span><br></pre></td></tr></table></figure><p>我说这个函数返回的是值是因为这个被函数创建的 <strong><em>user</em></strong> 值被拷贝并传递到调用栈上。这意味着调用函数接收到的是这个值的拷贝。</p><p>你可以看下第 17 行到 20 行 <strong><em>user</em></strong> 值被构造的过程。然后在第 23 行，<strong><em>user</em></strong> 值的副本被传递到调用栈并返回给调用者。函数返回后，栈看起来如下所示。</p><h3 id="图-1"><a href="#图-1" class="headerlink" title="图 1"></a>图 1</h3><p><img src="https://www.ardanlabs.com/blog/images/goinggo/81_figure1.png" alt=""></p><p>你可以看到图 1 中，当调用完 <strong><em>createUserV1</em></strong> ，一个 <strong><em>user</em></strong> 值同时存在（两个函数的）栈帧中。在函数版本 2 中，返回指针。</p><h3 id="表-3"><a href="#表-3" class="headerlink" title="表 3"></a>表 3</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, &amp;u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> &amp;u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>我说这个函数返回的是指针是因为这个被函数创建的 <strong><em>user</em></strong> 值通过调用栈被共享了。这意味着调用函数接收到一个值的地址拷贝。</p><p>你可以看到在第 28 行到 31 行使用相同的字段值来构造 <strong><em>user</em></strong> 值，但在第 34 行返回时却是不同的。不是将 <strong><em>user</em></strong> 值的副本传递到调用栈，而是将 <strong><em>user</em></strong> 值的地址传递到调用栈。基于此，你也许会认为栈在调用之后是这个样子。</p><h3 id="图-2"><a href="#图-2" class="headerlink" title="图 2"></a>图 2</h3><p><img src="https://www.ardanlabs.com/blog/images/goinggo/81_figure2.png" alt=""></p><p>如果看到的图 2 真的发生的话，你将遇到一个问题。指针指向了栈下的无效地址空间。当 <strong><em>main</em></strong> 函数调用下一个函数，指向的内存将重新映射并将被重新初始化。</p><p>这就是逃逸分析将开始保持完整性的地方。在这种情况下，编译器将检查到，在 <strong><em>createUserV2</em></strong> 的（函数）栈中构造 <strong><em>user</em></strong> 值是不安全的，因此，替代地，会在堆中构造（相应的）值。这（个分析并处理的过程）将在第 28 行构造时立即发生。</p><h2 id="可读性（Readability）"><a href="#可读性（Readability）" class="headerlink" title="可读性（Readability）"></a>可读性（Readability）</h2><p>在上一篇博文中，我们知道一个函数只能直接访问它的（函数栈）空间，或者通过（函数栈空间内的）指针，通过跳转访问（函数栈空间外的）外部内存。这意味着访问逃逸到堆上的值也需要通过指针跳转。</p><p>记住 <strong><em>createUserV2</em></strong> 的代码的样子</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, &amp;u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> &amp;u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>语法隐藏了代码中真正发生的事情。第 28 行声明的变量 <strong><em>u</em></strong> 代表一个 <strong><em>user</em></strong> 类型的值。Go 代码中的类型构造不会告诉你值在内存中的位置。所以直到第 34 行返回类型时，你才知道值需要逃逸（处理）。这意味着，虽然 <strong><em>u</em></strong> 代表类型 <strong><em>user</em></strong> 的一个值，但对该值的访问必须通过指针进行。</p><p>你可以在函数调用之后，看到堆栈就像（图 3）这样。</p><h3 id="图-3"><a href="#图-3" class="headerlink" title="图 3"></a>图 3</h3><p><img src="https://www.ardanlabs.com/blog/images/goinggo/81_figure3.png" alt=""></p><p>在 <strong><em>createUserV2</em></strong> 函数栈中，变量 <strong><em>u</em></strong> 代表的值存在于堆中，而不是栈。这意味着用 <strong><em>u</em></strong> 访问值时，使用指针访问而不是直接访问。你可能想，为什么不让 <strong><em>u</em></strong> 成为指针，毕竟访问它代表的值需要使用指针？</p><h3 id="表-5"><a href="#表-5" class="headerlink" title="表 5"></a>表 5</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := &amp;user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>如果你这样做，将使你的代码缺乏重要的可读性。（让我们）离开整个函数一秒，只关注 <strong><em>return</em></strong>。</p><h3 id="表-6"><a href="#表-6" class="headerlink" title="表 6"></a>表 6</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">34</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>这个 <strong><em>return</em></strong> 告诉你什么了呢？它说明了返回 <strong><em>u</em></strong> 值的副本给调用栈。然而，当你使用 <strong><em>&amp;</em></strong> 操作符，<strong><em>return</em></strong> 又告诉你什么了呢？</p><h3 id="表-7"><a href="#表-7" class="headerlink" title="表 7"></a>表 7</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">34</span>     <span class="keyword">return</span> &amp;u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>多亏了 <strong><em>&amp;</em></strong> 操作符，<strong><em>return</em></strong> 告诉你 <strong><em>u</em></strong> 被分享给调用者，因此，已经逃逸到堆中。记住，当你读代码的时候，指针是为了共享，<strong><em>&amp;</em></strong> 操作符对应单词 “sharing”。这在提高可读性的时候非常有用，这（也）是你不想失去的部分。</p><h3 id="表-8"><a href="#表-8" class="headerlink" title="表 8"></a>表 8</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">01</span> <span class="keyword">var</span> u *user</span><br><span class="line"><span class="number">02</span> err := json.Unmarshal([]<span class="keyword">byte</span>(r), &amp;u)</span><br><span class="line"><span class="number">03</span> <span class="keyword">return</span> u, err</span><br></pre></td></tr></table></figure><p>为了让其可以工作，你一定要通过共享指针变量（的方式）给（函数） <strong><em>json.Unmarshal</em></strong>。<strong>*json.Unmarshal 调用时会创建 *</strong>user*** 值并将其地址赋值给指针变量。<a href="https://play.golang.org/p/koI8EjpeIx" target="_blank" rel="noopener">https://play.golang.org/p/koI8EjpeIx</a></p><p>代码解释：<br>01：创建一个类型为 <strong><em>user</em></strong>，值为空的指针。<br>02：跟函数 <strong><em>json.Unmarshal</em></strong> 函数共享指针。<br>03：返回 <strong><em>u</em></strong> 的副本给调用者。</p><p>这里并不是很好理解，<strong><em>user</em></strong>值被 <strong><em>json.Unmarshal</em></strong> 函数创建，并被共享给调用者。</p><p>如何在构造过程中使用语法语义来改变可读性？</p><h3 id="表-9"><a href="#表-9" class="headerlink" title="表 9"></a>表 9</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">01</span> <span class="keyword">var</span> u user</span><br><span class="line"><span class="number">02</span> err := json.Unmarshal([]<span class="keyword">byte</span>(r), &amp;u)</span><br><span class="line"><span class="number">03</span> <span class="keyword">return</span> &amp;u, err</span><br></pre></td></tr></table></figure><p>代码解释：<br>01：创建一个类型为 <strong><em>user</em></strong>，值为空的变量。<br>02：跟函数 <strong><em>json.Unmarshal</em></strong> 函数共享 <strong><em>u</em></strong>。<br>03：跟调用者共享 <strong><em>u</em></strong>。</p><p>这里非常好理解。第 02 行共享 <strong><em>user</em></strong> 值到调用栈中的 <strong><em>json.Unmarshal</em></strong>，在第 03 行 <strong><em>user</em></strong> 值共享给调用者。这个共享过程将会导致 <strong><em>user</em></strong> 值逃逸。</p><p>在构建一个值时，使用值语义，并利用 <strong><em>&amp;</em></strong> 操作符的可读性来明确值是如何被共享的。</p><h2 id="编译器报告（Compiler-Reporting）"><a href="#编译器报告（Compiler-Reporting）" class="headerlink" title="编译器报告（Compiler Reporting）"></a>编译器报告（Compiler Reporting）</h2><p>想查看编译器（关于逃逸分析）的决定，你可以让编译器提供一份报告。你只需要在调用 <strong><em>go build</em></strong> 的时候，打开 <strong><em>-gcflags</em></strong> 开关，并带上 <strong><em>-m</em></strong> 选项。</p><p>实际上总共可以使用 4 个 <strong><em>-m</em></strong>，（但）超过 2 个级别的信息就已经太多了。我将使用 2 个 <strong><em>-m</em></strong> 的级别。</p><h3 id="表-10"><a href="#表-10" class="headerlink" title="表 10"></a>表 10</h3><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></pre></td><td class="code"><pre><span class="line">$ go build -gcflags &quot;-m -m&quot;</span><br><span class="line">.&#x2F;main.go:16: cannot inline createUserV1: marked go:noinline</span><br><span class="line">.&#x2F;main.go:27: cannot inline createUserV2: marked go:noinline</span><br><span class="line">.&#x2F;main.go:8: cannot inline main: non-leaf function</span><br><span class="line">.&#x2F;main.go:22: createUserV1 &amp;u does not escape</span><br><span class="line">.&#x2F;main.go:34: &amp;u escapes to heap</span><br><span class="line">.&#x2F;main.go:34: from ~r0 (return) at .&#x2F;main.go:34</span><br><span class="line">.&#x2F;main.go:31: moved to heap: u</span><br><span class="line">.&#x2F;main.go:33: createUserV2 &amp;u does not escape</span><br><span class="line">.&#x2F;main.go:12: main &amp;u1 does not escape</span><br><span class="line">.&#x2F;main.go:12: main &amp;u2 does not escape</span><br></pre></td></tr></table></figure><p>你可以看到编译器报告是否需要逃逸处理的决定。编译器都说了什么呢？请再看一下引用的 <strong><em>createUserV1</em></strong> 和 <strong><em>createUserV2</em></strong> 函数。</p><h3 id="表-13"><a href="#表-13" class="headerlink" title="表 13"></a>表 13</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">16</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV1</span><span class="params">()</span> <span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">17</span>     u := user&#123;</span><br><span class="line"><span class="number">18</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">19</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">20</span>     &#125;</span><br><span class="line"><span class="number">21</span></span><br><span class="line"><span class="number">22</span>     <span class="built_in">println</span>(<span class="string">"V1"</span>, &amp;u)</span><br><span class="line"><span class="number">23</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">24</span> &#125;</span><br><span class="line"></span><br><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, &amp;u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> &amp;u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>从报告中的这一行开始。</p><h3 id="表-14"><a href="#表-14" class="headerlink" title="表 14"></a>表 14</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.&#x2F;main.go:22: createUserV1 &amp;u does not escape</span><br></pre></td></tr></table></figure><p>这是说在函数 <strong><em>createUserV1</em></strong> 调用 <strong><em>println</em></strong> 不会造成 <strong><em>user</em></strong> 值逃逸到堆。这是必须检查的，因为它将会跟函数 <strong><em>println</em></strong> 共享（<strong><em>u</em></strong>）。</p><p>接下来看报告中的这几行。</p><h3 id="表-15"><a href="#表-15" class="headerlink" title="表 15"></a>表 15</h3><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></pre></td><td class="code"><pre><span class="line">.&#x2F;main.go:34: &amp;u escapes to heap</span><br><span class="line">.&#x2F;main.go:34: from ~r0 (return) at .&#x2F;main.go:34</span><br><span class="line">.&#x2F;main.go:31: moved to heap: u</span><br><span class="line">.&#x2F;main.go:33: createUserV2 &amp;u does not escape</span><br></pre></td></tr></table></figure><p>这几行是说，类型为 <strong><em>user</em></strong>，并在第 31 行被赋值的 <strong><em>u</em></strong> 的值，因为第 34 行的 <strong><em>return</em></strong> 逃逸。最后一行是说，跟之前一样，在 33 行调用 <strong><em>println</em></strong> 不会造成 <strong><em>user</em></strong> 值逃逸。</p><p>阅读这些报告可能让人感到困惑，（编译器）会根据所讨论的变量的类型是基于值类型还是指针类型而略有变化。</p><p>将 <strong><em>u</em></strong> 改为指针类型的 <strong><em>\</em>user*</strong>，而不是之前的命名类型 <strong><em>user</em></strong>。</p><h3 id="表-16"><a href="#表-16" class="headerlink" title="表 16"></a>表 16</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">27</span> <span class="function"><span class="keyword">func</span> <span class="title">createUserV2</span><span class="params">()</span> *<span class="title">user</span></span> &#123;</span><br><span class="line"><span class="number">28</span>     u := &amp;user&#123;</span><br><span class="line"><span class="number">29</span>         name:  <span class="string">"Bill"</span>,</span><br><span class="line"><span class="number">30</span>         email: <span class="string">"bill@ardanlabs.com"</span>,</span><br><span class="line"><span class="number">31</span>     &#125;</span><br><span class="line"><span class="number">32</span></span><br><span class="line"><span class="number">33</span>     <span class="built_in">println</span>(<span class="string">"V2"</span>, u)</span><br><span class="line"><span class="number">34</span>     <span class="keyword">return</span> u</span><br><span class="line"><span class="number">35</span> &#125;</span><br></pre></td></tr></table></figure><p>再次生成报告。</p><h3 id="表-17"><a href="#表-17" class="headerlink" title="表 17"></a>表 17</h3><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></pre></td><td class="code"><pre><span class="line">.&#x2F;main.go:30: &amp;user literal escapes to heap</span><br><span class="line">.&#x2F;main.go:30: from u (assigned) at .&#x2F;main.go:28</span><br><span class="line">.&#x2F;main.go:30: from ~r0 (return) at .&#x2F;main.go:34</span><br></pre></td></tr></table></figure><p>现在报告说在 28 行赋值的指针类型 <strong><em>\</em>user*</strong>，<strong><em>u</em></strong> 引用的 <strong><em>user</em></strong> 值，因为 34 行的 <strong><em>return</em></strong> 逃逸。</p><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>值在构建时并不能决定它将存在于哪里。只有当一个值被共享，编译器才能决定如何处理这个值。当你在调用时，共享了栈上的一个值时，它就会逃逸。在下一篇中你将探索一个值逃逸的其他原因。</p><p>这些文章试图引导你选择给定类型的值或指针的指导原则。每种方式都有（对应的）好处和（额外的）开销。保持在栈上的值，减少了 GC 的压力。但是需要存储，跟踪和维护不同的副本。将值放在堆上的指针，会增加 GC 的压力。然而，也有它的好处，只有一个值需要存储，跟踪和维护。（其实，）最关键的是如何保持正确地、一致地以及均衡（开销）地使用。</p><p>via: <a href="https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html" target="_blank" rel="noopener">https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html</a></p><p>作者：<a href="https://github.com/ardanlabs/gotraining" target="_blank" rel="noopener">William Kennedy</a><br>译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br>校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p>本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;前序（Prelude）&quot;&gt;&lt;a href=&quot;#前序（Prelude）&quot; class=&quot;headerlink&quot; title=&quot;前序（Prelude）&quot;&gt;&lt;/a&gt;前序（Prelude）&lt;/h2&gt;&lt;p&gt;本系列文章总共四篇，主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则，包括指针、栈、堆、逃逸分析和值/指针传递。这是第二篇，主要介绍堆和逃逸分析。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】Go 语言中的可赋值性</title>
    <link href="http://yoursite.com/2018/02/07/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%8F%AF%E8%B5%8B%E5%80%BC%E6%80%A7/"/>
    <id>http://yoursite.com/2018/02/07/%E3%80%90%E8%AF%91%E3%80%91Go-%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84%E5%8F%AF%E8%B5%8B%E5%80%BC%E6%80%A7/</id>
    <published>2018-02-07T12:46:14.000Z</published>
    <updated>2020-05-02T14:11:28.265Z</updated>
    
    <content type="html"><![CDATA[<p>Go 是静态类型的编程语言。存储到变量中的值必须与变量的类型匹配。因此，不能像 Python 这种动态类型语言一样，随意的数据都可以作为值赋给变量。这个决定什么是允许赋值的规则就叫做可赋值性（Assignability）。</p><a id="more"></a><p>左边类型为 T 的变量，存在 6 种可以将右边的值赋给左边的情况。</p><h3 id="1-相同类型（Identical-types）"><a href="#1-相同类型（Identical-types）" class="headerlink" title="1. 相同类型（Identical types）"></a>1. 相同类型（Identical types）</h3><p>第一种是非常明显的。如果右边的类型（也）是 T，则赋值是完全可以的。更多的细节可以参考 <a href="https://medium.com/golangspec/assignability-in-go-27805bcd5874" target="_blank" rel="noopener">“Identical types in Go”</a>。</p><h3 id="2-相同的基础类型（Identical-underlying-types）"><a href="#2-相同的基础类型（Identical-underlying-types）" class="headerlink" title="2. 相同的基础类型（Identical underlying types）"></a>2. 相同的基础类型（Identical underlying types）</h3><p>在 Go 语言中，每种类型都有一种基础类型（underlying type）。对于布尔型，数字，字符串或者常量的基础类型都跟它们本身的类型相同。其他的基础类型来自于声明时的类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> X <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> x X  <span class="comment">// underlying type is map[string]int</span></span><br></pre></td></tr></table></figure><p>可赋值的第二种情况是相同的基础类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> X <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> x X</span><br><span class="line"><span class="keyword">var</span> y <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line">x = y</span><br></pre></td></tr></table></figure><p>然而，如果有两个不同的命名类型（named types），则不能这么做：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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> X <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">type</span> Y <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> x X</span><br><span class="line"><span class="keyword">var</span> y Y</span><br><span class="line">x = y  <span class="comment">// cannot use y (type Y) as type X in assignment</span></span><br></pre></td></tr></table></figure><p>附加的条件是要求至少一个类型不是一个命名类型。</p><p>Go 中的变量要么是命名类型（named）要么是非命名类型（unnamed）。非命名类型（unnamed types）是指使用类型字面意思（语言本身）定义的类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a [<span class="number">10</span>]<span class="keyword">string</span></span><br><span class="line"><span class="keyword">var</span> b <span class="keyword">struct</span>&#123; field <span class="keyword">string</span>&#125;</span><br><span class="line"><span class="keyword">var</span> c <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br></pre></td></tr></table></figure><h3 id="3-将一个实现了接口-T-的变量赋值给-T-接口类型的变量"><a href="#3-将一个实现了接口-T-的变量赋值给-T-接口类型的变量" class="headerlink" title="3. 将一个实现了接口 T 的变量赋值给 T 接口类型的变量"></a>3. 将一个实现了接口 T 的变量赋值给 T 接口类型的变量</h3><p>如果一个变量实现了接口 T，那么我们可以将这个变量赋值给一个 T 接口类型的变量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Callable <span class="keyword">interface</span> &#123;</span><br><span class="line">   f() <span class="keyword">int</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> T <span class="keyword">int</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t T)</span> <span class="title">f</span><span class="params">()</span> <span class="title">int</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">int</span>(t)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">var</span> c Callable</span><br><span class="line"><span class="keyword">var</span> t T</span><br><span class="line">c = t</span><br></pre></td></tr></table></figure><p>更多关于接口类型的细节都在 <a href="https://golang.org/ref/spec#Interface_types" target="_blank" rel="noopener">语言规范（language spec）</a>。</p><h3 id="4-将双向管道（channel）的变量赋值给相同类型的变量（Assigning-bidirectional-channel-to-variable-with-identical-element-types）"><a href="#4-将双向管道（channel）的变量赋值给相同类型的变量（Assigning-bidirectional-channel-to-variable-with-identical-element-types）" class="headerlink" title="4. 将双向管道（channel）的变量赋值给相同类型的变量（Assigning bidirectional channel to variable with identical element types）"></a>4. 将双向管道（channel）的变量赋值给相同类型的变量（Assigning bidirectional channel to variable with identical element types）</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> T <span class="keyword">chan</span>&lt;- <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> c1 T</span><br><span class="line"><span class="keyword">var</span> c2 <span class="keyword">chan</span> <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line">c1 = c2</span><br><span class="line">c2 = c1  <span class="comment">// cannot use c1 (type T) as type chan map[string]int in assignment</span></span><br></pre></td></tr></table></figure><p>跟第二种情况（相同的基础类型）一样，要求至少一种管道（channel）变量是非命名类型（unnamed type）：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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> T <span class="keyword">chan</span>&lt;- <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">type</span> T2 <span class="keyword">chan</span> <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> c1 T</span><br><span class="line"><span class="keyword">var</span> c2 T2</span><br><span class="line">c1 = c2  <span class="comment">// cannot use c2 (type T2) as type T in assignment</span></span><br></pre></td></tr></table></figure><h3 id="5-赋值-nil（Assigning-nil）"><a href="#5-赋值-nil（Assigning-nil）" class="headerlink" title="5. 赋值 nil（Assigning nil）"></a>5. 赋值 nil（Assigning nil）</h3><p>允许将 nil 赋值给指针，函数，切片，map，管道，接口类型（的变量）。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a *<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> b <span class="function"><span class="keyword">func</span><span class="params">(<span class="keyword">int</span>)</span> <span class="title">int</span></span></span><br><span class="line"><span class="keyword">var</span> c []<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> d <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> e <span class="keyword">chan</span> <span class="keyword">int</span></span><br><span class="line"><span class="keyword">var</span> f <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">a, b, c, d, e, f = <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span></span><br><span class="line"><span class="keyword">var</span> g [<span class="number">10</span>]<span class="keyword">int</span></span><br><span class="line">g = <span class="literal">nil</span>  <span class="comment">// cannot use nil as type [10]int in assignment</span></span><br></pre></td></tr></table></figure><h3 id="6-无（显式）指定类型的常量（Untyped-constants）"><a href="#6-无（显式）指定类型的常量（Untyped-constants）" class="headerlink" title="6. 无（显式）指定类型的常量（Untyped constants）"></a>6. 无（显式）指定类型的常量（Untyped constants）</h3><p>关于 Go 常量更深入的介绍请查看<a href="https://blog.golang.org/constants" target="_blank" rel="noopener">官方博客</a></p><p>无（显式）指定类型的常量可以被赋值给常量所代表的类型 T 相同的类型为 T 的变量。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> a <span class="keyword">float32</span></span><br><span class="line"><span class="keyword">var</span> b <span class="keyword">float64</span></span><br><span class="line"><span class="keyword">var</span> c <span class="keyword">int32</span></span><br><span class="line"><span class="keyword">var</span> d <span class="keyword">int64</span></span><br><span class="line"><span class="keyword">const</span> untyped = <span class="number">1</span></span><br><span class="line">a = untyped</span><br><span class="line">b = untyped</span><br><span class="line">c = untyped</span><br><span class="line">d = untyped</span><br></pre></td></tr></table></figure><p>via: <a href="https://medium.com/golangspec/assignability-in-go-27805bcd5874" target="_blank" rel="noopener">https://medium.com/golangspec/assignability-in-go-27805bcd5874</a></p><p> 作者：<a href="https://twitter.com/mlowicki" target="_blank" rel="noopener">Michał Łowicki</a><br> 译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">Miancai Li</a><br> 校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p> 本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Go 是静态类型的编程语言。存储到变量中的值必须与变量的类型匹配。因此，不能像 Python 这种动态类型语言一样，随意的数据都可以作为值赋给变量。这个决定什么是允许赋值的规则就叫做可赋值性（Assignability）。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
  <entry>
    <title>【译】一个 NTP 客户端的 Go 实现</title>
    <link href="http://yoursite.com/2018/02/01/%E3%80%90%E8%AF%91%E3%80%91%E4%B8%80%E4%B8%AA-NTP-%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%9A%84-Go-%E5%AE%9E%E7%8E%B0/"/>
    <id>http://yoursite.com/2018/02/01/%E3%80%90%E8%AF%91%E3%80%91%E4%B8%80%E4%B8%AA-NTP-%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%9A%84-Go-%E5%AE%9E%E7%8E%B0/</id>
    <published>2018-02-01T12:32:59.000Z</published>
    <updated>2020-05-02T14:09:35.614Z</updated>
    
    <content type="html"><![CDATA[<p>在网络编程做了一些研究之后，我邂逅了一篇题目为《Let’s Make a NTP Client in C》，由 David Lettier（Lettier） 编写的文章。这篇文章鼓舞了我用 Go 去做相似的事。</p><a id="more"></a><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这篇博文提到的代码都在这里 [https:&#x2F;&#x2F;github.com&#x2F;vladimirvivien&#x2F;go-ntp-client](https:&#x2F;&#x2F;github.com&#x2F;vladimirvivien&#x2F;go-ntp-client.)</span><br></pre></td></tr></table></figure><p>这篇博文描述了一个（真正的） NTP 客户端的结构，使用 Go 语言编写。它通过　encoding/binary 库去封装，解封装，发送和接收来自远端 NTP 服务器基于 UDP 协议的 NTP 包。</p><p>你能通过<a href="http://www.ntp.org/" target="_blank" rel="noopener">这里</a>学到更多关于 NTP 协议的内容，或者阅读 RFC5905 规范、研究一个实现了更多的功能，（可能）比 Go NTP 客户端更好的客户端 <a href="https://github.com/beevik/ntp" target="_blank" rel="noopener">https://github.com/beevik/ntp</a>。</p><h2 id="NTP-包结构"><a href="#NTP-包结构" class="headerlink" title="NTP 包结构"></a>NTP 包结构</h2><p>时间同步的概念是非常复杂的，我还不能完全理解，也超过这篇博文的范围。但幸运的是，NTP 使用的数据包格式很简单，对于客户端来说也小而足够了。下面的图展示了 NTP v4 的包格式。关于这篇博文，我们只关注前 48 个字节，忽略掉 v4 版本的扩展部分。</p><p><img src="/images/NTP-v4-data-format.png" alt="NTP v4 data format (abbreviated) — https://tools.ietf.org/html/rfc5905"><br>NTP v4 data format (abbreviated) — <a href="https://tools.ietf.org/html/rfc5905" target="_blank" rel="noopener">https://tools.ietf.org/html/rfc5905</a></p><h2 id="NTP-包"><a href="#NTP-包" class="headerlink" title="NTP 包"></a>NTP 包</h2><p>客户端和对应的服务端都使用上面提到的相同的包格式。下面的结构体定义了 NTP 包和它的属性，跟上面提到的格式一一对应。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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">type</span> packet <span class="keyword">struct</span> &#123;</span><br><span class="line">    Settings       <span class="keyword">uint8</span>  <span class="comment">// leap yr indicator, ver number, and mode</span></span><br><span class="line">    Stratum        <span class="keyword">uint8</span>  <span class="comment">// stratum of local clock</span></span><br><span class="line">    Poll           <span class="keyword">int8</span>   <span class="comment">// poll exponent</span></span><br><span class="line">    Precision      <span class="keyword">int8</span>   <span class="comment">// precision exponent</span></span><br><span class="line">    RootDelay      <span class="keyword">uint32</span> <span class="comment">// root delay</span></span><br><span class="line">    RootDispersion <span class="keyword">uint32</span> <span class="comment">// root dispersion</span></span><br><span class="line">    ReferenceID    <span class="keyword">uint32</span> <span class="comment">// reference id</span></span><br><span class="line">    RefTimeSec     <span class="keyword">uint32</span> <span class="comment">// reference timestamp sec</span></span><br><span class="line">    RefTimeFrac    <span class="keyword">uint32</span> <span class="comment">// reference timestamp fractional</span></span><br><span class="line">    OrigTimeSec    <span class="keyword">uint32</span> <span class="comment">// origin time secs</span></span><br><span class="line">    OrigTimeFrac   <span class="keyword">uint32</span> <span class="comment">// origin time fractional</span></span><br><span class="line">    RxTimeSec      <span class="keyword">uint32</span> <span class="comment">// receive time secs</span></span><br><span class="line">    RxTimeFrac     <span class="keyword">uint32</span> <span class="comment">// receive time frac</span></span><br><span class="line">    TxTimeSec      <span class="keyword">uint32</span> <span class="comment">// transmit time secs</span></span><br><span class="line">    TxTimeFrac     <span class="keyword">uint32</span> <span class="comment">// transmit time frac</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="启动-UDP-连接"><a href="#启动-UDP-连接" class="headerlink" title="启动 UDP 连接"></a>启动 UDP 连接</h2><p>接下来，我们通过 UDP 协议，使用　net.Dial 函数去启动一个 socket，与 NTP 服务器联系，并设定 15 秒的超时时间。</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></pre></td><td class="code"><pre><span class="line">conn, err :&#x3D; net.Dial(&quot;udp&quot;, host)</span><br><span class="line">if err !&#x3D; nil &#123;</span><br><span class="line">    log.Fatal(&quot;failed to connect:&quot;, err)</span><br><span class="line">&#125;</span><br><span class="line">defer conn.Close()</span><br><span class="line">if err :&#x3D; conn.SetDeadline(</span><br><span class="line">  time.Now().Add(15 * time.Second)); err !&#x3D; nil &#123;</span><br><span class="line">    log.Fatal(&quot;failed to set deadline: &quot;, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="从服务端获取时间"><a href="#从服务端获取时间" class="headerlink" title="从服务端获取时间"></a>从服务端获取时间</h2><p>在发送请求包给服务端前，第一个字节是用来设置通信的配置，我们这里用 0x1B（或者二进制 00011011），代表客户端模式为 3，NTP版本为 3，润年为 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></pre></td><td class="code"><pre><span class="line"><span class="comment">// configure request settings by specifying the first byte as</span></span><br><span class="line"><span class="comment">// 00 011 011 (or 0x1B)</span></span><br><span class="line"><span class="comment">// |  |   +-- client mode (3)</span></span><br><span class="line"><span class="comment">// |  + ----- version (3)</span></span><br><span class="line"><span class="comment">// + -------- leap year indicator, 0 no warning</span></span><br><span class="line">req := &amp;packet&#123;Settings: <span class="number">0x1B</span>&#125;</span><br></pre></td></tr></table></figure><p>接下来，我们使用 binary 库去自动地将 packet 结构体封装成字节流，并以大端格式发送出去。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> err := binary.Write(conn, binary.BigEndian, req); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    log.Fatalf(<span class="string">"failed to send request: %v"</span>, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="从服务端读取时间"><a href="#从服务端读取时间" class="headerlink" title="从服务端读取时间"></a>从服务端读取时间</h2><p>接下来，我们使用 binary 包再次将从服务端读取的字节流自动地解封装成对应的 packet 结构体。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><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">rsp := &amp;packet&#123;&#125;</span><br><span class="line"><span class="keyword">if</span> err := binary.Read(conn, binary.BigEndian, rsp); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    log.Fatalf(<span class="string">"failed to read server response: %v"</span>, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="解析时间"><a href="#解析时间" class="headerlink" title="解析时间"></a>解析时间</h2><p>在这个超普通的例子里面，我们只对　Transmit Time 字段 （rsp.TxTimeSec 和 rspTxTimeFrac） 感兴趣，它们是从服务端发出时的时间。但我们不能直接使用它们，必须先转成 Unix 时间。</p><p>Unix 时间是一个开始于 1970 年的纪元（或者说从 1970 年开始的秒数）。然而 NTP 使用的是另外一个纪元，从 1900 年开始的秒数。因此，从 NTP 服务端获取到的值要正确地转成 Unix 时间必须减掉这 70 年间的秒数 （1970-1900），或者说 2208988800 秒。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><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">const</span> ntpEpochOffset = <span class="number">2208988800</span></span><br><span class="line">...</span><br><span class="line">secs := <span class="keyword">float64</span>(rsp.TxTimeSec) - ntpEpochOffset</span><br><span class="line">nanos := (<span class="keyword">int64</span>(rsp.TxTimeFrac) * <span class="number">1e9</span>) &gt;&gt; <span class="number">32</span></span><br></pre></td></tr></table></figure><p>NTP 值的分数部分转成纳秒。在这个平凡的例子里，这里是可选的，展示只是为了完整性。</p><h2 id="显示时间"><a href="#显示时间" class="headerlink" title="显示时间"></a>显示时间</h2><p>最后，函数 time.Unix 被用来创建一个秒数部分使用 secs，分数部分使用 nanos 值的时间。然后这个时间会被打印到终端。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fmt.Printf(<span class="string">"%v\n"</span>, time.Unix(<span class="keyword">int64</span>(secs), nanos))</span><br></pre></td></tr></table></figure><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>这篇博文展示了一个关于 NTP 客户端的普通的例子。描述了如何利用 encoding/binary 库，非常容易地将一个结构体转成字节形式。相反，我们使用 binary 库将一个字节流转成对应的结构体值。</p><p>这个 NTP 客户端还不是一个可用于生产环境的产品，毕竟它缺少了 NTP 规范指定的很多功能。从服务端返回的大部分字段都被忽略了。你可以从<a href="https://github.com/beevik/ntp" target="_blank" rel="noopener">这里</a>获取到一个用 Go 写的更完整的 NTP 客户端。</p><p>via: <a href="https://medium.com/learning-the-go-programming-language/lets-make-an-ntp-client-in-go-287c4b9a969f" target="_blank" rel="noopener">https://medium.com/learning-the-go-programming-language/lets-make-an-ntp-client-in-go-287c4b9a969f</a></p><p> 作者：<a href="https://twitter.com/VladimirVivien" target="_blank" rel="noopener">Vladimir Vivien</a><br> 译者：<a href="https://github.com/gogeof" target="_blank" rel="noopener">gogeof</a><br> 校对：<a href="https://github.com/polaris1119" target="_blank" rel="noopener">polaris1119</a></p><p> 本文由 <a href="https://github.com/studygolang/GCTT" target="_blank" rel="noopener">GCTT</a> 原创编译，<a href="https://studygolang.com/" target="_blank" rel="noopener">Go 中文网</a> 荣誉推出</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在网络编程做了一些研究之后，我邂逅了一篇题目为《Let’s Make a NTP Client in C》，由 David Lettier（Lettier） 编写的文章。这篇文章鼓舞了我用 Go 去做相似的事。&lt;/p&gt;
    
    </summary>
    
    
    
      <category term="翻译文章" scheme="http://yoursite.com/tags/%E7%BF%BB%E8%AF%91%E6%96%87%E7%AB%A0/"/>
    
  </entry>
  
</feed>
