<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>jianhao</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://sunshinechen.cn/</id>
  <link href="https://sunshinechen.cn/" rel="alternate"/>
  <link href="https://sunshinechen.cn/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, jianhao</rights>
  <subtitle>互联网相关 · 技术沉淀</subtitle>
  <title>sunshinechen的网络生活书</title>
  <updated>2026-10-28T09:18:36.426Z</updated>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Go" scheme="https://sunshinechen.cn/categories/Go/"/>
    <content>
      <![CDATA[<h1 id="go-plugin-插件系统详解"><a href="#go-plugin-插件系统详解" class="headerlink" title="go-plugin 插件系统详解"></a>go-plugin 插件系统详解</h1><p>基于 HashiCorp go-plugin 库的插件系统，通过 RPC 实现主程序（Host）与独立进程（Plugin）之间的通信。</p><h2 id="一、架构概览"><a href="#一、架构概览" class="headerlink" title="一、架构概览"></a>一、架构概览</h2><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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><code class="hljs dns">┌─────────────────┐         RPC          ┌─────────────────┐<br>│   Host <span class="hljs-punctuation">(</span>main<span class="hljs-punctuation">)</span>   │ ◄─────────────────► │  Plugin <span class="hljs-punctuation">(</span>子进程<span class="hljs-punctuation">)</span> │<br>│                 │                     │                 │<br>│ - 加载插件进程   │                     │ - 实现接口       │<br>│ - 调用 RPC      │                     │ - 提供服务       │<br>└─────────────────┘                     └─────────────────┘<br></code></pre></td></tr></table></figure><p><strong>核心特点</strong>：</p><ul><li>进程隔离：Plugin 崩溃不影响 Host</li><li>语言无关：Plugin 可以用任何语言实现（只要支持 RPC）</li><li>动态加载：运行时加载&#x2F;替换插件</li></ul><h2 id="二、核心组件"><a href="#二、核心组件" class="headerlink" title="二、核心组件"></a>二、核心组件</h2><h3 id="1-定义接口-shared-xxx-interface-go"><a href="#1-定义接口-shared-xxx-interface-go" class="headerlink" title="1. 定义接口 (shared&#x2F;xxx_interface.go)"></a>1. 定义接口 (shared&#x2F;xxx_interface.go)</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Provider <span class="hljs-keyword">interface</span> &#123;<br>Provide() <span class="hljs-type">string</span><br>&#125;<br></code></pre></td></tr></table></figure><p>该接口是 Plugin 端和 Host 端都需实现的。</p><h3 id="2-四个关键结构体的协作"><a href="#2-四个关键结构体的协作" class="headerlink" title="2. 四个关键结构体的协作"></a>2. 四个关键结构体的协作</h3><p>在示例中有4个结构体，一同协作实现了插件系统最核心的工作流；默认情况下, Host 作为 RPC 客户端，Plugin 作为 RPC 服务端提供服务。</p><table><thead><tr><th>结构体</th><th>位置</th><th>角色</th></tr></thead><tbody><tr><td><code>JsonProvider</code></td><td>Plugin 端</td><td>真实业务实现</td></tr><tr><td><code>ProvideRPCServer</code></td><td>Plugin 端</td><td>RPC 服务端，接收请求</td></tr><tr><td><code>ProvideClientRPC</code></td><td>Host 端</td><td>RPC 客户端代理，转发调用</td></tr><tr><td><code>ProvidePlugin</code></td><td>胶水层</td><td>连接两端的桥梁</td></tr></tbody></table><h3 id="协作流程图"><a href="#协作流程图" class="headerlink" title="协作流程图"></a>协作流程图</h3><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><code class="hljs dart">┌─────────────────────────── HOST 进程 ───────────────────────────┐<br>│                                                              │<br>│  <span class="hljs-title class_">ProvideClientRPC</span>                                            │<br>│  ┌─────────────────────────────────┐                         │<br>│  │  实现 <span class="hljs-title class_">Provider</span> 接口              │                         │<br>│  │  <span class="hljs-title class_">Provide</span>() 方法                 │                         │<br>│  │  内部持有 rpc.<span class="hljs-title class_">Client</span>           │ ──┐                     │<br>│  └─────────────────────────────────┘   │                     │<br>│                                        ▼                     │<br>│                              ┌──────────────────┐            │<br>│                              │  网络 RPC 调用   │            │<br>│                              └──────────────────┘            │<br>└────────────────────────────────────────────────────────────────┘<br>                                        │<br>                                        ▼<br>┌─────────────────────────── PLUGIN 进程 ───────────────────────┐<br>│                                                              │<br>│                              ┌──────────────────┐            │<br>│                              │  网络 RPC 服务   │            │<br>│                              └──────────────────┘            │<br>│                                        │                     │<br>│  <span class="hljs-title class_">ProvideRPCServer</span>                      ▼                     │<br>│  ┌─────────────────────────────────┐   │                     │<br>│  │  实现 net/rpc.<span class="hljs-title class_">Server</span> 接口      │   │                     │<br>│  │  <span class="hljs-title class_">Provide</span>() 方法                │◄──┘                     │<br>│  │  内部持有 <span class="hljs-title class_">JsonProvider</span>         │                         │<br>│  └─────────────────────────────────┘                         │<br>│            │                                                  │<br>│            ▼                                                  │<br>│  <span class="hljs-title class_">JsonProvider</span> (真实业务实现)                                  │<br>│  ┌─────────────────────────────────┐                         │<br>│  │  实现 <span class="hljs-title class_">Provider</span> 接口             │                         │<br>│  │  <span class="hljs-title class_">Provide</span>() 返回实际结果         │                         │<br>│  └─────────────────────────────────┘                         │<br>└────────────────────────────────────────────────────────────────┘<br><br><span class="hljs-title class_">ProvidePlugin</span> 是<span class="hljs-string">&quot;胶水&quot;</span>：<br>  • <span class="hljs-title class_">Server</span>() 方法 → 返回 <span class="hljs-title class_">ProvideRPCServer</span><br>  • <span class="hljs-title class_">Client</span>() 方法 → 返回 <span class="hljs-title class_">ProvideClientRPC</span><br></code></pre></td></tr></table></figure><p><strong>调用流程</strong>：</p><ol><li>Host 调用 <code>ProvideClientRPC.Provide()</code></li><li><code>ProvideClientRPC</code> 通过 <code>rpc.Client</code> 发送网络请求</li><li>Plugin 端的 <code>ProvideRPCServer.Provide()</code> 接收请求</li><li><code>ProvideRPCServer</code> 调用 <code>JsonProvider.Provide()</code> 获取真实结果</li><li>结果沿原路返回给 Host</li></ol><h3 id="3-Plugin-实现-shared-xxx-interface-go"><a href="#3-Plugin-实现-shared-xxx-interface-go" class="headerlink" title="3. Plugin 实现 (shared&#x2F;xxx_interface.go)"></a>3. Plugin 实现 (shared&#x2F;xxx_interface.go)</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> ProvidePlugin <span class="hljs-keyword">struct</span> &#123;<br>Impl Provider<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *ProvidePlugin)</span></span> Server(broker *plugin.MuxBroker) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;<br><span class="hljs-keyword">return</span> &amp;ProvideRPCServer&#123;Impl: p.Impl&#125;, <span class="hljs-literal">nil</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *ProvidePlugin)</span></span> Client(broker *plugin.MuxBroker, client *rpc.Client) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;<br><span class="hljs-keyword">return</span> &amp;ProvideClientRPC&#123;client: client&#125;, <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="4-Host-端-main-go"><a href="#4-Host-端-main-go" class="headerlink" title="4. Host 端 (main.go)"></a>4. Host 端 (main.go)</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// 启动插件进程</span><br>client := plugin.NewClient(&amp;plugin.ClientConfig&#123;<br>HandshakeConfig: handshakeConfig,<br>Plugins:         pluginMap,<br>Cmd:             exec.Command(<span class="hljs-string">&quot;./plugin/json&quot;</span>),<br>Logger:          logger,<br>&#125;)<br><span class="hljs-keyword">defer</span> client.Kill()<br><br><span class="hljs-comment">// 获取 RPC 客户端</span><br>rpcClient, err := client.Client()<br><br><span class="hljs-comment">// 提取插件实例</span><br>raw, err := rpcClient.Dispense(<span class="hljs-string">&quot;provider&quot;</span>)<br>greeter := raw.(shared.Provider)<br><br><span class="hljs-comment">// 调用插件</span><br>greeter.Provide()<br></code></pre></td></tr></table></figure><h3 id="5-Plugin-端-plugin-main-go"><a href="#5-Plugin-端-plugin-main-go" class="headerlink" title="5. Plugin 端 (plugin&#x2F;main.go)"></a>5. Plugin 端 (plugin&#x2F;main.go)</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br>jsonA := &amp;JsonProvider&#123;logger: logger&#125;<br><br>pluginMap := <span class="hljs-keyword">map</span>[<span class="hljs-type">string</span>]plugin.Plugin&#123;<br><span class="hljs-string">&quot;provider&quot;</span>: &amp;shared.ProvidePlugin&#123;Impl: jsonA&#125;,<br>&#125;<br><br>plugin.Serve(&amp;plugin.ServeConfig&#123;<br>HandshakeConfig: handshakeConfig,<br>Plugins:         pluginMap,<br>&#125;)<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="三、Dispense-的工作原理"><a href="#三、Dispense-的工作原理" class="headerlink" title="三、Dispense 的工作原理"></a>三、Dispense 的工作原理</h2><p><code>rpcClient.Dispense(&quot;provider&quot;)</code> 中的 <strong>“provider”</strong> 是 key：</p><table><thead><tr><th>位置</th><th>代码</th><th>说明</th></tr></thead><tbody><tr><td>Host 端</td><td><code>rpcClient.Dispense(&quot;provider&quot;)</code></td><td>请求 key 为 “provider” 的插件</td></tr><tr><td>Plugin 端</td><td><code>pluginMap = &#123;&quot;provider&quot;: &amp;ProvidePlugin&#123;...&#125;&#125;</code></td><td>用 “provider” 注册</td></tr></tbody></table><p><strong>流程</strong>：</p><ol><li>Host 调用 <code>Dispense(&quot;provider&quot;)</code></li><li>go-plugin 框架向 Plugin 发送请求，携带 key</li><li>Plugin 查找 <code>pluginMap[key]</code>，调用 <code>Server()</code> 返回 RPCServer</li><li>Host 端调用 <code>Client()</code> 获取代理对象</li><li>代理对象类型断言为接口使用</li></ol><h2 id="四、Handshake-机制"><a href="#四、Handshake-机制" class="headerlink" title="四、Handshake 机制"></a>四、Handshake 机制</h2><h3 id="HandshakeConfig-结构"><a href="#HandshakeConfig-结构" class="headerlink" title="HandshakeConfig 结构"></a>HandshakeConfig 结构</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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><code class="hljs go"><span class="hljs-keyword">var</span> handshakeConfig = plugin.HandshakeConfig&#123;<br>    ProtocolVersion:  <span class="hljs-number">1</span>,              <span class="hljs-comment">// 协议版本</span><br>    MagicCookieKey:   <span class="hljs-string">&quot;BASIC_PLUGIN&quot;</span>, <span class="hljs-comment">// Cookie 键</span><br>    MagicCookieValue: <span class="hljs-string">&quot;hello&quot;</span>, <span class="hljs-comment">// Cookie 值</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="三个属性的作用"><a href="#三个属性的作用" class="headerlink" title="三个属性的作用"></a>三个属性的作用</h3><table><thead><tr><th>属性</th><th>作用</th></tr></thead><tbody><tr><td><strong>ProtocolVersion</strong></td><td>校验 Host 和 Plugin 的协议版本是否一致，不一致则拒绝连接</td></tr><tr><td><strong>MagicCookieKey</strong></td><td>用于验证的 key（相当于校验字段名）</td></tr><tr><td><strong>MagicCookieValue</strong></td><td>用于验证的 value（相当于校验密码）</td></tr></tbody></table><h3 id="工作流程"><a href="#工作流程" class="headerlink" title="工作流程"></a>工作流程</h3><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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><code class="hljs dart"><span class="hljs-title class_">Host</span> 启动 <span class="hljs-title class_">Plugin</span> 进程<br>       ↓<br>发送 <span class="hljs-title class_">HandshakeConfig</span> 信息（包含 version、cookie）<br>       ↓<br><span class="hljs-title class_">Plugin</span> 验证：<br>  - version 是否匹配？<br>  - cookie key/value 是否正确？<br>       ↓<br>验证失败 → 显示友好错误信息 → 进程退出<br>验证成功 → 建立 RPC 连接<br></code></pre></td></tr></table></figure><h3 id="验证失败的场景"><a href="#验证失败的场景" class="headerlink" title="验证失败的场景"></a>验证失败的场景</h3><ol><li><strong>版本不匹配</strong> - Host 用 v1，Plugin 用 v2</li><li><strong>误执行插件目录</strong> - 用户直接运行 <code>./plugin/</code> 而不是可执行文件</li><li><strong>错误的插件</strong> - 加载了不相关的可执行文件</li></ol><h3 id="示例错误信息"><a href="#示例错误信息" class="headerlink" title="示例错误信息"></a>示例错误信息</h3><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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><code class="hljs dart">错误的 <span class="hljs-title class_">Plugin</span>：<br>  <span class="hljs-title class_">This</span> plugin <span class="hljs-keyword">is</span> not a <span class="hljs-title class_">HashiCorp</span> go-plugin plugin. <br>  <span class="hljs-title class_">It</span> appears to be a directory or other invalid plugin.<br><br>版本不匹配：<br>  <span class="hljs-title class_">Incompatible</span> api version. <span class="hljs-title class_">Plugin</span>: <span class="hljs-number">2</span>, <span class="hljs-title class_">Host</span>: <span class="hljs-number">1</span><br></code></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ul><li><strong>不是安全机制</strong> - Cookie 是明文传输，可被伪造</li><li><strong>是防错机制</strong> - 防止用户因为操作错误导致困惑</li><li><strong>建议保持一致</strong> - Host 和 Plugin 两端的 HandshakeConfig 应完全相同</li></ul><h2 id="五、双向通信"><a href="#五、双向通信" class="headerlink" title="五、双向通信"></a>五、双向通信</h2><table><thead><tr><th>方向</th><th>是否支持</th><th>说明</th></tr></thead><tbody><tr><td>Host → Plugin</td><td>✅ 默认</td><td>Plugin 提供服务</td></tr><tr><td>Plugin → Host</td><td>✅ 可选</td><td>需用 MuxBroker 建立回调通道</td></tr></tbody></table><p>通过 <code>MuxBroker</code> 可以让 Plugin 主动回调 Host。</p><h2 id="六、性能开销"><a href="#六、性能开销" class="headerlink" title="六、性能开销"></a>六、性能开销</h2><table><thead><tr><th>调用方式</th><th>延迟</th></tr></thead><tbody><tr><td>直接函数调用</td><td>~1-10 ns</td></tr><tr><td>go-plugin RPC</td><td>~50-500 μs</td></tr><tr><td>本地网络 RPC</td><td>~100μs - 1ms</td></tr></tbody></table><p><strong>开销来源</strong>：</p><ul><li>序列化（gob 编码）</li><li>进程间通信（socket）</li><li>上下文切换</li><li>反射开销</li></ul><p><strong>适用场景</strong>：调用频率不高的、需要进程隔离的场景。</p><h2 id="七、”Plugin-Provide”-中的-“Plugin”-是什么？"><a href="#七、”Plugin-Provide”-中的-“Plugin”-是什么？" class="headerlink" title="七、”Plugin.Provide” 中的 “Plugin” 是什么？"></a>七、”Plugin.Provide” 中的 “Plugin” 是什么？</h2><p>在 <code>provider_interface.go:19</code> 中：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs go">err := p.client.Call(<span class="hljs-string">&quot;Plugin.Provide&quot;</span>, <span class="hljs-built_in">new</span>(<span class="hljs-keyword">interface</span>&#123;&#125;), &amp;resp)<br></code></pre></td></tr></table></figure><p><code>&quot;Plugin.Provide&quot;</code> 是 net&#x2F;rpc 的调用格式：<code>&quot;类型名.方法名&quot;</code>。</p><p>那为什么是 “Plugin.Provide” ？</p><p><strong>“Plugin” 是 go-plugin 框架硬编码的服务名</strong>。客户端调用 <code>&quot;Plugin.Provide&quot;</code> 正是因为服务端用<br><code>server.RegisterName(&quot;Plugin&quot;, impl)</code> 注册了服务。</p><h3 id="源码位置"><a href="#源码位置" class="headerlink" title="源码位置"></a>源码位置</h3><p>在 go-plugin 库的 <code>rpc_server.go:195</code> 行：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">()</span></span> &#123;<br>...<br>serve(conn, <span class="hljs-string">&quot;Plugin&quot;</span>, impl) <span class="hljs-comment">// 硬编码 &quot;Plugin&quot;</span><br>&#125;()<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">serve</span><span class="hljs-params">(conn io.ReadWriteCloser, name <span class="hljs-type">string</span>, v <span class="hljs-keyword">interface</span>&#123;&#125;)</span></span> &#123;<br>server := rpc.NewServer()<br><span class="hljs-keyword">if</span> err := server.RegisterName(name, v); err != <span class="hljs-literal">nil</span> &#123;<br>...<br>&#125;<br>server.ServeConn(conn)<br>&#125;<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2026/03/11/go-plugin,%20Go%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%8F%92%E4%BB%B6%E5%BA%93/</id>
    <link href="https://sunshinechen.cn/2026/03/11/go-plugin,%20Go%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%8F%92%E4%BB%B6%E5%BA%93/"/>
    <published>2026-03-11T02:58:58.000Z</published>
    <summary>
      <![CDATA[<h1 id="go-plugin-插件系统详解"><a href="#go-plugin-插件系统详解" class="headerlink" title="go-plugin 插件系统详解"></a>go-plugin 插件系统详解</h1><p>基于 HashiCorp g]]>
    </summary>
    <title>go-plugin 插件系统详解</title>
    <updated>2026-10-28T09:18:36.426Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="Ubuntu" scheme="https://sunshinechen.cn/tags/Ubuntu/"/>
    <category term="Wireguard" scheme="https://sunshinechen.cn/tags/Wireguard/"/>
    <content>
      <![CDATA[<h2 id="实现目标"><a href="#实现目标" class="headerlink" title="实现目标"></a>实现目标</h2><p>以云服务器作为中转节点，使用 WireGuard 建立点对点的加密隧道，实现外网安全访问家庭内网。</p><h2 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h2><h3 id="云服务器"><a href="#云服务器" class="headerlink" title="云服务器"></a>云服务器</h3><p>这里使用 Ubuntu 22.04，有独立公网 ip，能放行指定 TCP&#x2F;UDP 端口。</p><h3 id="家庭客户端"><a href="#家庭客户端" class="headerlink" title="家庭客户端"></a>家庭客户端</h3><ul><li>家庭电脑（Linux &#x2F; Wiindows）</li><li>支持 WireGuard 的路由器</li></ul><h2 id="云服务器配置"><a href="#云服务器配置" class="headerlink" title="云服务器配置"></a>云服务器配置</h2><h3 id="安装-WireGuard"><a href="#安装-WireGuard" class="headerlink" title="安装 WireGuard"></a>安装 WireGuard</h3><p>通过 ssh 连接到云服务器，执行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">apt upgrade<br>apt update<br>apt install -y wireguard<br></code></pre></td></tr></table></figure><p>安装成功后输入以下命令，确认 WireGuard 安装成功：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">wg version<br><br><span class="hljs-comment">## 输出</span><br>wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/<br></code></pre></td></tr></table></figure><h3 id="安装-WireGuard-UI"><a href="#安装-WireGuard-UI" class="headerlink" title="安装 WireGuard-UI"></a>安装 WireGuard-UI</h3><p>WireGuard 是根据配置文件来进行管理的，对于非技术人员来说非常不友好，所以选择一个 UI 界面项目来管理 WIreGuard。</p><h4 id="下载"><a href="#下载" class="headerlink" title="下载"></a>下载</h4><p><a href="https://github.com/ngoduykhanh/wireguard-ui">WireGuard-UI</a> 是在 Github 中相关项目最多赞的，界面美观性、实用性都很好，所以选择这个项目来管理我们的 WireGuard。使用以下命令去进行下载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 目前我是安装在/etc/wireguard目录下</span><br><span class="hljs-built_in">cd</span> /etc/wireguard<br><span class="hljs-built_in">mkdir</span> ui &amp;&amp; <span class="hljs-built_in">cd</span> ui<br><br>wget https://github.com/ngoduykhanh/wireguard-ui/releases/download/v0.6.0/wireguard-ui-v0.6.0-linux-386.tar.gz<br><br><span class="hljs-comment">## 解压文件</span><br>tar -zxvf wireguard-ui-v0.6.0-linux-386.tar.gz<br><br><span class="hljs-comment">## 解压后会在当前文件生成一个名为wireguard-ui的二进制文件</span><br><span class="hljs-built_in">ls</span><br><span class="hljs-comment">## 输出</span><br>wireguard-ui  wireguard-ui-v0.6.0-linux-386.tar.gz<br></code></pre></td></tr></table></figure><h4 id="创建配置文件"><a href="#创建配置文件" class="headerlink" title="创建配置文件"></a>创建配置文件</h4><p>创建 WireGuard-UI 的配置文件，该项目是用 <code>.env</code> 环境变量的方式去管理配置，所有的配置项点击这里进行查看；这里为了方便展示，选用一个最简单就能运行的配置项：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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><code class="hljs bash">vim .<span class="hljs-built_in">env</span><br><br><span class="hljs-comment">## 编辑.env文件内容</span><br><br><span class="hljs-comment">#配置绑定IP和端口，默认为5000</span><br>BIND_ADDRESS=0.0.0.0:11194                                                                  <span class="hljs-comment"># ui的登录账户</span><br>WGUI_USERNAME=admin<br><span class="hljs-comment"># ui的登录账户密码</span><br>WGUI_PASSWORD=jh826708<br></code></pre></td></tr></table></figure><h4 id="配置开机启动以及参数配置"><a href="#配置开机启动以及参数配置" class="headerlink" title="配置开机启动以及参数配置"></a>配置开机启动以及参数配置</h4><p>下载之后使用 <a href="/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%881%EF%BC%89/">Systemd</a> 进行管理，使用以下命令创建 Service 文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs bash">vim /lib/systemd/system/wireguard-ui.service<br><br><br><span class="hljs-comment">## 粘贴以下内容到文件内</span><br>[Unit]<br>Description=WireGuard UI Daemon<br>Wants=network-online.target<br>After=network-online.target<br><br>[Service]<br>User=root<br>Group=root<br>Type=simple<br>WorkingDirectory=/etc/wireguard/ui<br>EnvironmentFile=/etc/wireguard/ui/.env<br>ExecStart=/etc/wireguard/ui/wireguard-ui<br><br>[Install]<br>WantedBy=multi-user.target<br></code></pre></td></tr></table></figure><p>配置开机启动</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs bash">systemctl daemon-reload<br>systemctl start wireguard-ui.service<br>systemctl <span class="hljs-built_in">enable</span> wireguard-ui.service<br>systemctl status wireguard-ui.service<br></code></pre></td></tr></table></figure><p>启动后会在 <code>/etc/wireguard</code> 目录下创建 <code>wg0.conf</code> 配置文件，查看该文件的 Listen Port，修改你想要监听的见口，然后去云服务器处放行该端口，这样就可以在公网访问到该 UI 界面了。</p><h4 id="设置当配置文件修改时，自动重启-WireGuard"><a href="#设置当配置文件修改时，自动重启-WireGuard" class="headerlink" title="设置当配置文件修改时，自动重启 WireGuard"></a>设置当配置文件修改时，自动重启 WireGuard</h4><p>WireGuard 在有一个新的客户端要连接时，都会修改配置文件添加 Peer 的信息，还要手动敲入命令进行重启，这就很麻烦。虽然 WireGuard-UI 只负责配置信息生成，但也需要我们手动的重启 WireGuard。</p><p>我们可以使用 Systemd 对 WireGuard 的配置文件进行监听，只要我们通过 WireGuard-UI 添加或者删除了 Peer，就会重新生成 WireGuard 的配置文件，Systemd 就会监听到这个操作，自动的帮我们重启 WireGuard。</p><p>使用 Systemd 实现文件监听和发生事件后的脚本操作：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs bash"><span class="hljs-comment">## 该服务文件是监听wireguard配置文件的修改事件</span><br>vim /lib/systemd/system/wgui.path<br><br><span class="hljs-comment">## 编辑文件</span><br>[Unit]<br>Description=监听/etc/wireguard/wg0.conf文件的改动<br><br>[Path]<br>PathModified=/etc/wireguard/wg0.conf<br><br>[Install]                                                                                   WantedBy=multi-user.target<br><br><br><span class="hljs-comment">## 当配置文件发生时所做的操作</span><br>vim /lib/systemd/system/wgui.service<br><br>[Unit]                                                                                      Description=当监听到/etc/wireguard/wg0.conf文件的改动时,重启 WireGuard                          After=network.target<br>[Service]                                                                                   Type=oneshot                                                                                ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service<br><br>[Install]                                                                                   RequiredBy=wgui.path<br></code></pre></td></tr></table></figure><p><code>wgui.service</code> 文件内的 <code>ExecStar</code> 选项中，是对 <code>wg-quick@wg0.service</code> 进行重启，<code>@</code>符号后的<code>wg0</code>字符 对应 WireGuard 位于 <code>/etc/wireguard</code> 目录下的配置文件名称；比如现在我是对 <code>wg-quick@wg0.service</code> 进行重启，所以我服务端 <code>/etc/wireguard</code> 目录下的配置名称就是叫 <code>wg0.conf</code>。</p><blockquote><p>具体的 Systemd 语法请看 <a href="/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%881%EF%BC%89/">Systemd详解（1）</a></p></blockquote><h3 id="访问、管理、添加客户端配置"><a href="#访问、管理、添加客户端配置" class="headerlink" title="访问、管理、添加客户端配置"></a>访问、管理、添加客户端配置</h3><h4 id="浏览器打开"><a href="#浏览器打开" class="headerlink" title="浏览器打开"></a>浏览器打开</h4><p>访问你云服务器的公网 IP 以及对应的端口</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_11-30.png"></p><p>输入 <code>.env</code> 配置文件里面的账户密码</p><h4 id="修改管理员默认密码"><a href="#修改管理员默认密码" class="headerlink" title="修改管理员默认密码"></a>修改管理员默认密码</h4><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_11-37.png"></p><h4 id="WireGuard-服务端配置"><a href="#WireGuard-服务端配置" class="headerlink" title="WireGuard 服务端配置"></a>WireGuard 服务端配置</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment">## Post Up Script</span><br>iptables -I FORWARD 1 -i wg0 -j ACCEPT; iptables -I FORWARD 2 -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE<br><br><span class="hljs-comment">## Post Down Script</span><br>iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE<br></code></pre></td></tr></table></figure><p>将上面的脚本命令粘贴到下图中</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_11-43.png"></p><p>修改后再页面右上角点击 <code>Apply Config</code> 按钮生成新的配置文件，因为之前也生成了对配置文件的监听，所以当配置文件有内容上的更新时，就会重新 WireGuard 服务。</p><h4 id="新建客户端"><a href="#新建客户端" class="headerlink" title="新建客户端"></a>新建客户端</h4><p>按照下图所示步骤添加客户端</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_13-35.png"></p><p>添加后就可以通过 <code>Download</code> 和 <code>QR Code</code> 按钮下载配置文件到客户端。</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_13-38.png"></p><h2 id="家庭客户端-1"><a href="#家庭客户端-1" class="headerlink" title="家庭客户端"></a>家庭客户端</h2><p>家庭客户端我这里只演示 Linux（Ubuntu 22.04）的，其他平台就不演示了。</p><h3 id="安装-WireGuard-1"><a href="#安装-WireGuard-1" class="headerlink" title="安装 WireGuard"></a>安装 WireGuard</h3><p>安装 WireGuard 步骤和云服务器侧一致，这里就不赘述了。</p><h3 id="新建配置文件"><a href="#新建配置文件" class="headerlink" title="新建配置文件"></a>新建配置文件</h3><p>在 <code>/etc/wireguard</code> 目录下新建 <code>wg0.conf</code> 配置文件，文件内容就是在 Web 界面中下载的内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs bash"><br>vim /etc/wireguard/wg0.conf<br><br><span class="hljs-comment">## 文件内容</span><br>[Interface]                                                                                 Address = 10.252.1.1/32                                                                     PrivateKey = 密钥内容<br>DNS = 1.1.1.1,8.8.8.8                                                                       MTU = 1450<br><br>[Peer]                                                                                      PublicKey = 公钥内容<br>PresharedKey = aiKWUo3g33V8kwOldFjOeVwR8O2OtfnbCf+I9xvMBMM=<br>AllowedIPs = 0.0.0.0/0<br>Endpoint = 云服务公网地址:51820<br>PersistentKeepalive = 60<br></code></pre></td></tr></table></figure><p>然后执行以下命令启动 WireGuard：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> wg-quick up wg0<br></code></pre></td></tr></table></figure><p>执行成功后在 Web 界面就可以看到对应的客户端上线了</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2026-01-05_13-46.png"></p><p>到现在 WireGuard 服务就搭建完成了，后面要是有新的客户端要接入，就在 Web 界面中新建客户端，填写好信息后提交。在下载该配置文件到客户端中，使用 WireGuard 命令完成连接。连接后就可以访问对方的网络了。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2026/01/05/%E5%9F%BA%E4%BA%8EWireguard%E7%9A%84%E5%AE%B6%E5%BA%AD%E5%86%85%E7%BD%91%E5%AE%89%E5%85%A8%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE%E6%96%B9%E6%A1%88/</id>
    <link href="https://sunshinechen.cn/2026/01/05/%E5%9F%BA%E4%BA%8EWireguard%E7%9A%84%E5%AE%B6%E5%BA%AD%E5%86%85%E7%BD%91%E5%AE%89%E5%85%A8%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE%E6%96%B9%E6%A1%88/"/>
    <published>2026-01-05T01:50:27.000Z</published>
    <summary>
      <![CDATA[<h2 id="实现目标"><a href="#实现目标" class="headerlink" title="实现目标"></a>实现目标</h2><p>以云服务器作为中转节点，使用 WireGuard 建立点对点的加密隧道，实现外网安全访问家庭内网。</p>
<h2 id="]]>
    </summary>
    <title>基于Wireguard的家庭内网安全远程访问方案</title>
    <updated>2026-10-28T09:18:36.262Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="运维/Docker" scheme="https://sunshinechen.cn/tags/%E8%BF%90%E7%BB%B4-Docker/"/>
    <content>
      <![CDATA[<h3 id="docker-镜像同步"><a href="#docker-镜像同步" class="headerlink" title="docker 镜像同步"></a>docker 镜像同步</h3><p><a href="https://github.com/psviderski/unregistry">unregistry</a> | <a href="https://github.com/psviderski/unregistry/releases/latest">下载地址</a></p><p>像 rsync 一样同步 Docker 镜像。该项目可通过简单的 docker pussh 命令，将本地 Docker 镜像直接推送到远程服务器，无需依赖外部容器镜像仓库。仅传输缺失的镜像层，比 docker save&#x2F;load 更快。</p><p>访问下载地址，将具体版本的二进制文件下载，该二进制文件名称是 <code>docker-pussh</code>，每次手敲都很麻烦。我将该文件移动到 PATH 路径中，方便直接访问。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/12/29/Docker%E5%AE%9E%E7%94%A8%E5%B7%A5%E5%85%B7%E5%90%88%E9%9B%86/</id>
    <link href="https://sunshinechen.cn/2025/12/29/Docker%E5%AE%9E%E7%94%A8%E5%B7%A5%E5%85%B7%E5%90%88%E9%9B%86/"/>
    <published>2025-12-29T02:02:27.000Z</published>
    <summary>好用的docker自用工具合集</summary>
    <title>Docker实用工具合集</title>
    <updated>2026-10-28T09:18:36.334Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux专" scheme="https://sunshinechen.cn/categories/Linux%E4%B8%93/"/>
    <category term="运维/Shell" scheme="https://sunshinechen.cn/tags/%E8%BF%90%E7%BB%B4-Shell/"/>
    <content>
      <![CDATA[<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># check root  </span><br>[[ <span class="hljs-variable">$EUID</span> -ne 0 ]] &amp;&amp; <span class="hljs-built_in">echo</span> -e <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;red&#125;</span>错误: <span class="hljs-variable">$&#123;plain&#125;</span> 必须使用root用户运行此脚本！\n&quot;</span> &amp;&amp; <span class="hljs-built_in">exit</span> 1<br></code></pre></td></tr></table></figure><p>这句shell命令的作用是检查当前用户是否为root用户，如果不是则输出错误信息并退出脚本。<br>具体分析如下：</p><ul><li><p><code>[[ $EUID -ne 0 ]]</code> ：这是一个条件判断语句</p><ul><li>$EUID 是shell内置变量，表示当前用户的有效用户ID（Effective User ID）</li><li>-ne 是数值比较操作符，表示”不等于”</li><li>0 是root用户的用户ID</li><li>整个条件的意思是：如果当前用户的EUID不等于0（即不是root用户）</li></ul></li><li><p>&amp;&amp;：逻辑与操作符，只有前面的条件为真时才会执行后面的命令</p></li><li><p>echo -e “${red}错误: ${plain} 必须使用root用户运行此脚本！\n”：输出错误信息</p><ul><li>${red} 和 ${plain} 是之前定义的颜色变量，用于设置输出文本的颜色</li><li>-e 参数允许解释反斜杠转义字符</li><li>\n 表示换行</li></ul></li><li><p>exit 1：以错误码1退出脚本，表示执行失败</p></li></ul><p>这是Linux系统管理脚本中常见的权限检查机制，确保只有具有足够权限的root用户才能执行后续的操作。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/12/12/%E6%A3%80%E6%9F%A5%E5%BD%93%E5%89%8D%E7%94%A8%E6%88%B7%E6%98%AF%E5%90%A6%E4%B8%BAroot%E7%94%A8%E6%88%B7/</id>
    <link href="https://sunshinechen.cn/2025/12/12/%E6%A3%80%E6%9F%A5%E5%BD%93%E5%89%8D%E7%94%A8%E6%88%B7%E6%98%AF%E5%90%A6%E4%B8%BAroot%E7%94%A8%E6%88%B7/"/>
    <published>2025-12-12T02:15:09.000Z</published>
    <summary>在Shell脚本中，叫你如何检查执行脚本时是否有root权限</summary>
    <title>检查当前用户是否为root用户</title>
    <updated>2026-10-28T09:18:36.290Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Git" scheme="https://sunshinechen.cn/tags/Git/"/>
    <content>
      <![CDATA[<blockquote><p>注意，本文章里的修改指的是<strong>纯本地、未推送、仅修改最近一次提交</strong>的注释信息；<br>如果是修改非最近提交、已经推送到远程仓库的 commit 内容，请慎重操作！</p></blockquote><p>使用以下命令即可修改 commit 信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">git commit --amend -m <span class="hljs-string">&quot;新的正确注释&quot;</span><br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2025/12/09/%E5%9C%A8Git%E4%B8%AD%E4%BF%AE%E6%94%B9%E6%9C%80%E8%BF%91%E4%B8%80%E6%AC%A1%E6%8F%90%E4%BA%A4%E4%B8%AD%E7%9A%84%E6%B3%A8%E9%87%8A%E4%BF%A1%E6%81%AF/</id>
    <link href="https://sunshinechen.cn/2025/12/09/%E5%9C%A8Git%E4%B8%AD%E4%BF%AE%E6%94%B9%E6%9C%80%E8%BF%91%E4%B8%80%E6%AC%A1%E6%8F%90%E4%BA%A4%E4%B8%AD%E7%9A%84%E6%B3%A8%E9%87%8A%E4%BF%A1%E6%81%AF/"/>
    <published>2025-12-09T02:41:53.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>注意，本文章里的修改指的是<strong>纯本地、未推送、仅修改最近一次提交</strong>的注释信息；<br>如果是修改非最近提交、已经推送到远程仓库的 commit 内容，请慎重操作！</p>
</blockquote>
<p>使用以下命令即]]>
    </summary>
    <title>在Git中修改最近一次提交中的注释信息</title>
    <updated>2026-10-28T09:18:36.430Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="Echo0"><a href="#Echo0" class="headerlink" title="Echo0"></a>Echo0</h3><p>清爽的轻量级内容分享平台。这是一款开源、自托管的轻量级内容发布平台，专注于思想流动和快速分享。</p><p>该项目有对依赖注入框架 <code>wire</code> 的最佳实践，在构建 Go 版本的 MVC 架构代码有很大参考价值。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/12/09/%E5%80%BC%E5%BE%97%E5%AD%A6%E4%B9%A0%E5%93%81%E4%BD%8D%E7%9A%84%E5%BA%93/</id>
    <link href="https://sunshinechen.cn/2025/12/09/%E5%80%BC%E5%BE%97%E5%AD%A6%E4%B9%A0%E5%93%81%E4%BD%8D%E7%9A%84%E5%BA%93/"/>
    <published>2025-12-09T02:32:54.000Z</published>
    <summary>
      <![CDATA[<h3 id="Echo0"><a href="#Echo0" class="headerlink" title="Echo0"></a>Echo0</h3><p>清爽的轻量级内容分享平台。这是一款开源、自托管的轻量级内容发布平台，专注于思想流动和快速分享。</p>
<p>该项目]]>
    </summary>
    <title>值得学习品位的库</title>
    <updated>2026-10-28T09:18:36.414Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="运维/Shell" scheme="https://sunshinechen.cn/tags/%E8%BF%90%E7%BB%B4-Shell/"/>
    <content>
      <![CDATA[<h2 id="参数替换"><a href="#参数替换" class="headerlink" title="参数替换"></a>参数替换</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs Bash"><span class="hljs-meta">#!/bin/bash</span><br><br>TARGET_IP=<span class="hljs-variable">$&#123;1:-&quot;8.8.8.8&quot;&#125;</span>       <span class="hljs-comment"># 目标IP，默认8.8.8.8</span><br>MAX_RETRIES=<span class="hljs-variable">$&#123;2:-0&#125;</span>            <span class="hljs-comment"># 最大重试次数，0表示无限</span><br>RETRY_INTERVAL=<span class="hljs-variable">$&#123;3:-1&#125;</span>         <span class="hljs-comment"># 重试间隔（秒）</span><br>VERBOSE=<span class="hljs-variable">$&#123;4:-false&#125;</span>            <span class="hljs-comment"># 是否输出详细信息</span><br><br>retries=0<br><br><span class="hljs-keyword">while</span> <span class="hljs-literal">true</span>; <span class="hljs-keyword">do</span><br>  <span class="hljs-keyword">if</span> ping -c 1 -W 2 <span class="hljs-string">&quot;<span class="hljs-variable">$TARGET_IP</span>&quot;</span> &gt; /dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span><br>    <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;[SUCCESS] Connected to <span class="hljs-variable">$TARGET_IP</span>&quot;</span><br>    <span class="hljs-built_in">exit</span> 0<br>  <span class="hljs-keyword">else</span><br>    <span class="hljs-variable">$VERBOSE</span> &amp;&amp; <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;[INFO] Connection to <span class="hljs-variable">$TARGET_IP</span> failed. Retrying...&quot;</span><br>    ((retries++))<br>    <span class="hljs-comment"># 超时检测</span><br>    <span class="hljs-keyword">if</span> [[ <span class="hljs-variable">$MAX_RETRIES</span> -gt 0 &amp;&amp; <span class="hljs-variable">$retries</span> -ge <span class="hljs-variable">$MAX_RETRIES</span> ]]; <span class="hljs-keyword">then</span><br>      <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;[ERROR] Failed after <span class="hljs-variable">$MAX_RETRIES</span> attempts. Exiting.&quot;</span><br>      <span class="hljs-built_in">exit</span> 1<br>    <span class="hljs-keyword">fi</span><br>    <span class="hljs-built_in">sleep</span> <span class="hljs-string">&quot;<span class="hljs-variable">$RETRY_INTERVAL</span>&quot;</span><br>  <span class="hljs-keyword">fi</span><br><span class="hljs-keyword">done</span><br><br></code></pre></td></tr></table></figure><p><code>check_net.sh</code> 代码实现了一个检测网络连接的功能，会持续尝试ping通 8.8.8.8，直到最大尝试重试次数并退出。其中的 <code>TARGET_IP=$&#123;1:-&quot;8.8.8.8&quot;&#125;</code> 里，就用到Bash Shell的参数替换功能。</p><h2 id="语法结构"><a href="#语法结构" class="headerlink" title="语法结构"></a>语法结构</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-variable">$&#123;参数位置:-默认值&#125;</span><br></code></pre></td></tr></table></figure><ul><li><strong>参数位置</strong>：<code>1</code> 表示第一个命令行参数（<code>$1</code>）</li><li><strong><code>:-</code></strong>：特殊符号，表示”如果前面的变量未设置或值为空”</li><li><strong>默认值</strong>：当前面的变量不满足条件时使用的值</li></ul><h2 id="典型场景"><a href="#典型场景" class="headerlink" title="典型场景"></a>典型场景</h2><h3 id="情况1：不传参数"><a href="#情况1：不传参数" class="headerlink" title="情况1：不传参数"></a>情况1：不传参数</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">./check_net.sh<br><span class="hljs-comment"># $1 未定义 → 使用默认值 → TARGET_IP=8.8.8.8</span><br></code></pre></td></tr></table></figure><h3 id="情况2：传入空字符串"><a href="#情况2：传入空字符串" class="headerlink" title="情况2：传入空字符串"></a>情况2：传入空字符串</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">./check_net.sh <span class="hljs-string">&quot;&quot;</span><br><span class="hljs-comment"># $1 是空值 → 使用默认值 → TARGET_IP=8.8.8.8</span><br></code></pre></td></tr></table></figure><h3 id="情况3：传入有效值"><a href="#情况3：传入有效值" class="headerlink" title="情况3：传入有效值"></a>情况3：传入有效值</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">./check_net.sh <span class="hljs-string">&quot;192.168.1.1&quot;</span><br><span class="hljs-comment"># $1 有值 → 使用传入值 → TARGET_IP=192.168.1.1</span><br></code></pre></td></tr></table></figure><h2 id="其他的参数替换"><a href="#其他的参数替换" class="headerlink" title="其他的参数替换"></a>其他的参数替换</h2><p>在Shell中，还有其他的参数替换功能可用</p><table><thead><tr><th align="center">语法</th><th align="center">触发条件</th></tr></thead><tbody><tr><td align="center">${变量:-默认值}</td><td align="center">变量未定义或值为空</td></tr><tr><td align="center">${变量-默认值}</td><td align="center">仅变量未定义（允许空值）</td></tr><tr><td align="center">${变量:&#x3D;默认值}</td><td align="center">变量未定义时赋值（永久设置）</td></tr><tr><td align="center">${变量:?错误信息}</td><td align="center">变量未定义&#x2F;为空时终止脚本，显示错误</td></tr><tr><td align="center"><code>$&#123;变量： -$(命令)&#125;</code></td><td align="center">变量未定义时使用命令生成</td></tr></tbody></table><h2 id="链式默认值"><a href="#链式默认值" class="headerlink" title="链式默认值"></a>链式默认值</h2><p>在一个变量没有设置时，有多个变量候选设置，这种情况就可以使用链式默认值</p><p>语法：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 优先用用户指定值 → 次选环境变量 → 最后用硬编码值</span><br>log_level=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;1:-<span class="hljs-variable">$&#123;LOG_LEVEL:-info&#125;</span>&#125;</span>&quot;</span><br></code></pre></td></tr></table></figure><p>实际代码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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><code class="hljs bash"><span class="hljs-meta">#!/bin/bash</span><br><br><span class="hljs-comment"># 变量来源可以是任意途径（参数、环境变量、配置文件等）</span><br>INPUT_NAME=<span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span>          <span class="hljs-comment"># 来自参数</span><br>ENV_NAME=<span class="hljs-string">&quot;<span class="hljs-variable">$USER</span>&quot;</span>         <span class="hljs-comment"># 来自环境变量</span><br>CONFIG_NAME=$(get_config) <span class="hljs-comment"># 来自某个命令的输出</span><br><br><span class="hljs-comment"># 统一使用参数替换处理</span><br>display_name=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;INPUT_NAME:-<span class="hljs-variable">$&#123;ENV_NAME:-Guest&#125;</span>&#125;</span>&quot;</span><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Welcome, <span class="hljs-variable">$display_name</span>&quot;</span><br></code></pre></td></tr></table></figure><table><thead><tr><th>测试命令</th><th>输出结果</th><th>处理逻辑</th></tr></thead><tbody><tr><td>.&#x2F;demo.sh</td><td>Welcome, 当前用户名</td><td>参数未传 → 使用环境变量USER→如果USER → 如果USER→如果USER未定义则使用”Guest”</td></tr><tr><td>.&#x2F;demo.sh “”</td><td>Welcome, Guest</td><td>参数传空字符串 → 由于变量INPUT_NAME为空 → 触发默认值链式替换</td></tr><tr><td>unset USER; .&#x2F;demo.sh</td><td>Welcome, Guest</td><td>参数未传+环境变量$USER未定义 → 最终使用”Guest”</td></tr><tr><td>.&#x2F;demo.sh “Alice”</td><td>Welcome, Alice</td><td>参数优先 → 使用显式传入值</td></tr></tbody></table><h2 id="代码中其他参数的解释"><a href="#代码中其他参数的解释" class="headerlink" title="代码中其他参数的解释"></a>代码中其他参数的解释</h2><p>通过对第一个参数的解析，其他参数也就知道是什么意思了</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">MAX_RETRIES=<span class="hljs-variable">$&#123;2:-0&#125;</span>      <span class="hljs-comment"># 如果没给第2个参数 → 用0（无限重试）</span><br>RETRY_INTERVAL=<span class="hljs-variable">$&#123;3:-1&#125;</span>   <span class="hljs-comment"># 如果没给第3个参数 → 用1秒</span><br>VERBOSE=<span class="hljs-variable">$&#123;4:-false&#125;</span>      <span class="hljs-comment"># 如果没给第4个参数 → 用false</span><br></code></pre></td></tr></table></figure><p>支持多种调用方式</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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><code class="hljs bash"><span class="hljs-comment"># 基础用法（使用所有默认值）</span><br>./check_net.sh<br><br><span class="hljs-comment"># 只指定目标IP（其他用默认值）</span><br>./check_net.sh 192.168.1.1<br><br><span class="hljs-comment"># 指定所有参数</span><br>./check_net.sh 192.168.1.1 10 2 <span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2025/11/27/Shell%E7%9A%84%E5%8F%82%E6%95%B0%E6%9B%BF%E6%8D%A2/</id>
    <link href="https://sunshinechen.cn/2025/11/27/Shell%E7%9A%84%E5%8F%82%E6%95%B0%E6%9B%BF%E6%8D%A2/"/>
    <published>2025-11-27T01:25:39.000Z</published>
    <summary>
      <![CDATA[<h2 id="参数替换"><a href="#参数替换" class="headerlink" title="参数替换"></a>参数替换</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre]]>
    </summary>
    <title>Shell的参数替换</title>
    <updated>2026-10-28T09:18:36.290Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="Nginx" scheme="https://sunshinechen.cn/tags/Nginx/"/>
    <content>
      <![CDATA[<p>要查看 Nginx 的 log 文件，先要查看 Nginx 的配置文件位置，使用以下命令查看</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">nginx -t<br><br><span class="hljs-comment">## 输出</span><br>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok<br>nginx: configuration file /etc/nginx/nginx.conf <span class="hljs-built_in">test</span> is successful<br></code></pre></td></tr></table></figure><p>以上输出代表 Nginx 所使用的配置文件是 <code>/etc/nginx/nginx.conf</code>。</p><p>在 Nginx 中，日志文件分为 <code>access.log</code> 和 <code>error.log</code>，分别代表访问 nginx 的日志和错误日志。在配置文件中会以以下的方式去配置日志路径：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs nginx.conf">error_log  logs/error.log  info;<br>access_log logs/access.log info;<br></code></pre></td></tr></table></figure><p>如果配置文件里面路径是以相对路径去显示，那么日志文件的路径就以 <code>编译参数指定路径</code> + <code>配置文件里的相对路径</code>。<br>如果是绝对路径，那么日志文件就会生成到这个路径里：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs nginx.conf">error_log  /var/nginx/logs/error.log  info;<br>access_log /var/nginx/logs/access.log info;<br></code></pre></td></tr></table></figure><p>那么，编译参数指定路径是怎么获取的呢？通过以下命令获取：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">nginx -V 2&gt;&amp;1 | grep --color -- <span class="hljs-string">&#x27;--prefix&#x27;</span><br><br><span class="hljs-comment">## 输出</span><br>configure arguments: --with-cc-opt=<span class="hljs-string">&#x27;-g -O2 -ffile-prefix-map=/build/nginx-aSXKE0/nginx-1.18.0=. -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat-Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2&#x27;</span> --with-ld-opt=<span class="hljs-string">&#x27;-Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -Wl,-z,now -fPIC&#x27;</span> --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_r<br>ealip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --add-dynamic-module=/build/nginx-aSXKE0/nginx-1.18.0/debian/modules/http-geoip2 --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module<br></code></pre></td></tr></table></figure><p>该命令会筛选<code>--prefix</code>的字符串并高亮显示，这样就可以获取到 Nginx 的 prefix 编译参数，然后再与上一步配置文件中的相对路径进行拼接，这样就获取到了 Nginx 的日志全路径；</p><p>如果得出的路径还没有日志，则可以获取 <code>--http-log-path</code> 和 <code>--error-log-path</code> 这 2 个编译参数值，也是查找路径之一；</p><p>如果通过包管理器（如 apt&#x2F;yum）安装 Nginx，默认会标准化路径（如 &#x2F;var&#x2F;log&#x2F;nginx&#x2F;），直接遵循该规则即可。</p><blockquote><p>如果配置文件中使用了绝对路径（如 &#x2F;var&#x2F;log&#x2F;error.log），日志文件一定会生成到该路径；如果使用了相对路径（如 logs&#x2F;error.log），则实际路径是 编译时的 –prefix + 相对路径。<br>配置文件中的绝对路径优先级最高，编译参数无法覆盖它。</p></blockquote>]]>
    </content>
    <id>https://sunshinechen.cn/2025/11/13/%E6%9F%A5%E7%9C%8BNginx%E7%9A%84log%E6%96%87%E4%BB%B6%E4%BD%8D%E7%BD%AE/</id>
    <link href="https://sunshinechen.cn/2025/11/13/%E6%9F%A5%E7%9C%8BNginx%E7%9A%84log%E6%96%87%E4%BB%B6%E4%BD%8D%E7%BD%AE/"/>
    <published>2025-11-13T02:52:58.000Z</published>
    <summary>在Ubunut中如何查看Nginx的日志文件位置</summary>
    <title>Nginx查看log文件位置</title>
    <updated>2026-10-28T09:18:36.262Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">formatBytes</span><span class="hljs-params">(b <span class="hljs-type">uint64</span>)</span></span> <span class="hljs-type">string</span> &#123;<br><span class="hljs-keyword">const</span> unit = <span class="hljs-number">1024</span><br><span class="hljs-keyword">if</span> b &lt; unit &#123;<br><span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;%d B&quot;</span>, b)<br>&#125;<br>div, exp := unit, <span class="hljs-number">0</span><br><span class="hljs-keyword">for</span> n := b / unit; n &gt;= unit; n /= unit &#123;<br>div *= unit<br>exp++<br>&#125;<br><span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;%.2f %cB&quot;</span>, <span class="hljs-type">float64</span>(b)/<span class="hljs-type">float64</span>(div), <span class="hljs-string">&quot;KMGTPE&quot;</span>[exp])<br>&#125;<br></code></pre></td></tr></table></figure><p>这段 Go 语言代码定义了一个 <code>formatBytes</code> 函数，用于将字节大小（<code>uint64</code> 类型）转换为易读的字符串格式，例如 <code>1.23 KB</code>、<code>4.56 GB</code> 等。具体逻辑如下：</p><h3 id="单位定义"><a href="#单位定义" class="headerlink" title="单位定义"></a>单位定义</h3><p>使用二进制单位（<code>1024</code> 进位），默认单位为 <code>B</code>（字节）。</p><ul><li>单位后缀列表 <code>&quot;KMGTPE&quot;</code> 依次对应：<ul><li><code>K</code> (KiB &#x3D; 1024¹)</li><li><code>M</code> (MiB &#x3D; 1024²)</li><li><code>G</code> (GiB &#x3D; 1024³)</li><li><code>T</code> (TiB &#x3D; 1024⁴)</li><li><code>P</code> (PiB &#x3D; 1024⁵)</li><li><code>E</code> (EiB &#x3D; 1024⁶)</li></ul></li></ul><h3 id="小字节直接饭回"><a href="#小字节直接饭回" class="headerlink" title="小字节直接饭回"></a>小字节直接饭回</h3><figure class="highlight go"><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><code class="hljs go"><span class="hljs-keyword">if</span> b &lt; unit &#123;<br>    <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;%d B&quot;</span>, b)<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="动态选择最大单位"><a href="#动态选择最大单位" class="headerlink" title="动态选择最大单位"></a>动态选择最大单位</h3><p>通过循环找到最大的合适单位，这也是这个函数的核心部分</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">for</span> n := b / unit; n &gt;= unit; n /= unit &#123;<br>div *= unit<br>exp++<br>&#125;<br></code></pre></td></tr></table></figure><p>假设 <code>b = 3*1024³</code>（即 3 GiB）：</p><ol><li><p><strong>初始状态</strong></p><ul><li><code>div = 1024</code>（单位基数从 KB 开始）</li><li><code>exp = 0</code>（单位索引对应 <code>K</code>）</li></ul></li><li><p><strong>第一次循环</strong></p><ul><li><code>n := b / unit = (3*1024³) / 1024 = 3*1024²</code> → <code>n = 3*1024²</code></li><li>检查条件 <code>n &gt;= unit</code> → <code>3*1024² &gt;= 1024</code> → <strong>成立</strong></li><li>执行循环体：<ul><li><code>div *= 1024</code> → <code>div = 1024²</code>（现在基数对应 <strong>MB</strong>）</li><li><code>exp++</code> → <code>exp=1</code>（索引现在指向 <code>M</code>）</li></ul></li><li>更新 <code>n = 3*1024² / 1024 = 3*1024</code> → <code>n=3072</code></li></ul></li><li><p><strong>第二次循环</strong></p><ul><li>检查条件 <code>n &gt;= unit</code> → <code>3072 &gt;= 1024</code> → <strong>成立</strong></li><li>执行循环体：<ul><li><code>div *= 1024</code> → <code>div = 1024³</code>（基数对应 <strong>GB</strong>）</li><li><code>exp++</code> → <code>exp=2</code>（索引指向 <code>G</code>）</li></ul></li><li>更新 <code>n = 3072 / 1024 = 3</code></li></ul></li><li><p><strong>第三次循环</strong></p><ul><li>检查条件 <code>n &gt;= unit</code> → <code>3 &gt;= 1024</code> → <strong>不成立</strong></li><li><strong>循环终止</strong></li></ul></li></ol><h3 id="格式化结果"><a href="#格式化结果" class="headerlink" title="格式化结果"></a>格式化结果</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;%.2f %cB&quot;</span>, <span class="hljs-type">float64</span>(b)/<span class="hljs-type">float64</span>(div), <span class="hljs-string">&quot;KMGTPE&quot;</span>[exp])<br></code></pre></td></tr></table></figure><p>将字节数除以单位基数（<code>div</code>），并附加对应的单位符号，保留两位小数。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/11/07/%E5%AD%97%E8%8A%82%E6%95%B0%E8%BD%AC%E6%8D%A2%E4%B8%BA%20KB%E3%80%81MB%E3%80%81GB%20%E7%AD%89%E6%9B%B4%E6%98%93%E8%AF%BB%E7%9A%84%E6%A0%BC%E5%BC%8F/</id>
    <link href="https://sunshinechen.cn/2025/11/07/%E5%AD%97%E8%8A%82%E6%95%B0%E8%BD%AC%E6%8D%A2%E4%B8%BA%20KB%E3%80%81MB%E3%80%81GB%20%E7%AD%89%E6%9B%B4%E6%98%93%E8%AF%BB%E7%9A%84%E6%A0%BC%E5%BC%8F/"/>
    <published>2025-11-07T08:57:28.000Z</published>
    <summary>
      <![CDATA[<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="l]]>
    </summary>
    <title>字节数转换为 KB、MB、GB 等更易读的格式</title>
    <updated>2026-10-28T09:18:36.410Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="Ubuntu" scheme="https://sunshinechen.cn/tags/Ubuntu/"/>
    <content>
      <![CDATA[<h3 id="实现-SSH-免密登录以及别名"><a href="#实现-SSH-免密登录以及别名" class="headerlink" title="实现 SSH 免密登录以及别名"></a>实现 SSH 免密登录以及别名</h3><p>使用以下命令</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">ssh-copy-id -i &#x27;替换成你的公钥目录&#x27; 用户名@ip地址<br></code></pre></td></tr></table></figure><p>使用命令之后需要输入密码，输入成功后就可以实现免密登录。</p><h3 id="实现-SSH-别名"><a href="#实现-SSH-别名" class="headerlink" title="实现 SSH 别名"></a>实现 SSH 别名</h3><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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><code class="hljs crmsh"><span class="hljs-comment">## 以下是 ~/.ssh/config 文件的内容</span><br>Host myserver  <span class="hljs-comment"># 自定义别名（如：dev/prod等）</span><br>    HostName <span class="hljs-number">192.168</span>.<span class="hljs-number">1.100</span>  <span class="hljs-comment"># 服务器IP或域名</span><br>    <span class="hljs-keyword">User</span> <span class="hljs-title">root</span>               <span class="hljs-comment"># 登录用户名</span><br>    Port <span class="hljs-number">22</span>                 <span class="hljs-comment"># 非默认端口时需指定</span><br>    IdentityFile ~/.ssh/id_rsa  <span class="hljs-comment"># 私钥路径</span><br></code></pre></td></tr></table></figure><p>结合免密就可以实现快速登录远程主机。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/10/28/%E5%AE%9E%E7%8E%B0%20SSH%20%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95%E5%92%8C%20SSH%20%E5%88%AB%E5%90%8D/</id>
    <link href="https://sunshinechen.cn/2025/10/28/%E5%AE%9E%E7%8E%B0%20SSH%20%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95%E5%92%8C%20SSH%20%E5%88%AB%E5%90%8D/"/>
    <published>2025-10-28T06:34:37.000Z</published>
    <summary>首页摘要</summary>
    <title>实现 SSH 免密登录和 SSH 别名</title>
    <updated>2026-10-28T09:18:36.438Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="Ubuntu" scheme="https://sunshinechen.cn/tags/Ubuntu/"/>
    <content>
      <![CDATA[<p>在终端下执行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">gsettings <span class="hljs-built_in">set</span> org.gnome.desktop.interface clock-show-seconds <span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2025/10/28/Ubuntu%2022.04%20%E7%B3%BB%E7%BB%9F%E4%BF%AE%E6%94%B9%E9%A1%B6%E9%83%A8%E6%97%B6%E9%97%B4%E6%A0%BC%E5%BC%8F%E4%B8%BA%E7%A7%92/</id>
    <link href="https://sunshinechen.cn/2025/10/28/Ubuntu%2022.04%20%E7%B3%BB%E7%BB%9F%E4%BF%AE%E6%94%B9%E9%A1%B6%E9%83%A8%E6%97%B6%E9%97%B4%E6%A0%BC%E5%BC%8F%E4%B8%BA%E7%A7%92/"/>
    <published>2025-10-28T06:32:01.000Z</published>
    <summary>看在Ubuntu系统中，如何将顶部时间格式修改为秒</summary>
    <title>Ubuntu 22.04 系统修改顶部时间格式为秒</title>
    <updated>2026-10-28T09:18:36.438Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<h3 id="什么是-ldflags-？"><a href="#什么是-ldflags-？" class="headerlink" title="什么是 ldflags ？"></a>什么是 <code>ldflags</code> ？</h3><p><code>ldflags</code> 是 Go 编译命令 <code>go build</code> 中的一个选项，用来船底给链接器额外的参数。通过调整这些参数，我们可以控制 Go 程序生成的二进制文件的大小、调试信息、符号表信息、动态注入变量等行为。</p><p>常见的<code>go build</code>命令示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-s -w&quot;</span><br></code></pre></td></tr></table></figure><h3 id="常见的-ldflags-参数详解"><a href="#常见的-ldflags-参数详解" class="headerlink" title="常见的-ldflags 参数详解"></a>常见的<code>-ldflags</code> 参数详解</h3><h4 id="s：移除符号表"><a href="#s：移除符号表" class="headerlink" title="-s：移除符号表"></a><code>-s</code>：移除符号表</h4><ul><li><strong>作用</strong>：掉符号表（symbol table）。符号表包括函数和变量的名字信息，主要用于调试和分析生成的二进制文件。删除符号表可以显著减少文件大小。</li><li><strong>适用场景</strong>：如果程序不需要调试符号信息，可以使用这个选项减小二进制文件体积。</li><li><strong>影响</strong>：调试时无法获取函数和变量名。</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs Bash">go build -ldflags <span class="hljs-string">&quot;-s&quot;</span><br></code></pre></td></tr></table></figure><h4 id="w：移除DWARF调试信息"><a href="#w：移除DWARF调试信息" class="headerlink" title="-w：移除DWARF调试信息"></a><code>-w</code>：移除DWARF调试信息</h4><ul><li><strong>作用</strong>：移除 DWARF 格式的调试信息。DWARF 是一种用于调试的标准，它包含关于程序的调试信息（例如变量类型、堆栈跟踪等）。</li><li><strong>适用场景</strong>：当你不需要保留调试信息时，删除这些信息可以进一步缩小二进制文件。</li><li><strong>影响</strong>：会影响程序的调试、堆栈跟踪和性能剖析等功能。</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-w&quot;</span><br></code></pre></td></tr></table></figure><h4 id="X-importpath-name-vlaue-：动态注入变量"><a href="#X-importpath-name-vlaue-：动态注入变量" class="headerlink" title="-X &#39;importpath.name=vlaue&#39;：动态注入变量"></a><code>-X &#39;importpath.name=vlaue&#39;</code>：动态注入变量</h4><ul><li><strong>作用</strong>：允许在编译时向 Go 程序中的全局字符串变量注入值。常用于在构建时设置应用的版本号、编译时间等。</li><li><strong>适用场景</strong>：在生成的二进制文件中注入动态信息，而无需硬编码在源码中。</li><li><strong>示例</strong>：假设代码中有一个 <code>var version string</code> 变量，使用以下命令可以将 <code>version</code> 变量设置为 <code>1.0.0</code>：</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflars <span class="hljs-string">&quot;-X &#x27;main.version=1.0.0&#x27;&quot;</span><br></code></pre></td></tr></table></figure><h4 id="trimpath：移除编译路径信息"><a href="#trimpath：移除编译路径信息" class="headerlink" title="-trimpath：移除编译路径信息"></a><code>-trimpath</code>：移除编译路径信息</h4><ul><li><strong>作用</strong>：在默认情况下，Go 编译器会将源码的绝对路径包含在生成的二进制文件中。<code>-trimpath</code> 用于移除这些路径信息，以防止敏感路径信息泄露，并使二进制文件更具可移植性。</li><li><strong>适用场景</strong>：当需要生成独立于编译环境的二进制文件，或避免泄露源码路径时，可以使用此选项。</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-trimpath&quot;</span><br></code></pre></td></tr></table></figure><h4 id="compressdwarf：压缩DWARF调试信息"><a href="#compressdwarf：压缩DWARF调试信息" class="headerlink" title="-compressdwarf：压缩DWARF调试信息"></a><code>-compressdwarf</code>：压缩DWARF调试信息</h4><ul><li><strong>作用</strong>：压缩保留的 DWARF 调试信息。这个选项不会移除调试信息，但通过压缩减少它们占用的空间。</li><li><strong>适用场景</strong>：当你需要保留调试信息，但希望尽量减少二进制文件大小时，可以使用此选项。</li><li><strong>支持版本</strong>：Go 1.15 及以上版本支持。</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags=<span class="hljs-string">&quot;-compressdwarf&quot;</span><br></code></pre></td></tr></table></figure><h4 id="linkmode-mode-：指定链接模式"><a href="#linkmode-mode-：指定链接模式" class="headerlink" title="-linkmode &#39;mode&#39;：指定链接模式"></a><code>-linkmode &#39;mode&#39;</code>：指定链接模式</h4><ul><li><p><strong>作用</strong>：<code>-linkmode</code> 参数用于指定链接器的工作模式。Go 提供三种链接模式：</p><ul><li><code>internal</code>：使用 Go 内置的链接器（默认）。</li><li><code>external</code>：使用系统链接器（如 <code>gcc</code>）。</li><li><code>auto</code>：Go 编译器自动选择合适的链接模式。</li></ul></li><li><p><strong>适用场景</strong>：在特定的嵌入式环境或系统环境中，你可能需要指定使用外部链接器来处理特定的库或符号。</p></li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-linkmode external&quot;</span><br></code></pre></td></tr></table></figure><h4 id="buildid-id：自定义构建ID"><a href="#buildid-id：自定义构建ID" class="headerlink" title="buildid &#39;id：自定义构建ID"></a><code>buildid &#39;id</code>：自定义构建ID</h4><ul><li><strong>作用</strong>：Go 编译器默认会生成一个唯一的 <code>build id</code>，用于标识每个二进制文件。<code>-buildid</code> 选项允许开发者自定义这个 <code>build id</code>，以便在多个构建版本之间进行标识。</li><li><strong>适用场景</strong>：用于版本管理或者标记不同的构建版本。</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-buildid=customid&quot;</span><br></code></pre></td></tr></table></figure><h4 id="extld-path-：指定外部链接器"><a href="#extld-path-：指定外部链接器" class="headerlink" title="extld &#39;path&#39;：指定外部链接器"></a><code>extld &#39;path&#39;</code>：指定外部链接器</h4><ul><li><strong>作用</strong>：配合 <code>-linkmode external</code> 使用，指定外部链接器（如 <code>gcc</code>）的路径。通常用于嵌入式开发或对链接器有特殊要求的场景。</li><li><strong>示例</strong>：指定使用 <code>/usr/bin/gcc</code> 作为外部链接器：</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;extld=/usr/bin/gcc&quot;</span><br></code></pre></td></tr></table></figure><h3 id="ldflags-参数组合示例"><a href="#ldflags-参数组合示例" class="headerlink" title="-ldflags 参数组合示例"></a><code>-ldflags</code> 参数组合示例</h3><h4 id="最小化二进制文件大小"><a href="#最小化二进制文件大小" class="headerlink" title="最小化二进制文件大小"></a>最小化二进制文件大小</h4><p>如果目标是生成尽量小的二进制文件，可以使用 <code>-s</code> 和 <code>-w</code> 组合，移除所有符号表和调试信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-s -w&quot;</span><br></code></pre></td></tr></table></figure><h4 id="在编译时注入版本信息和构建时间"><a href="#在编译时注入版本信息和构建时间" class="headerlink" title="在编译时注入版本信息和构建时间"></a>在编译时注入版本信息和构建时间</h4><p>在实际项目中，我们经常需要在运行时查看应用的版本号或构建时间。可以通过 <code>-X</code> 动态注入这些信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-X &#x27;main.version=1.2.3&#x27; -X &#x27;main.buildTime=<span class="hljs-subst">$(date)</span>&#x27;&quot;</span><br></code></pre></td></tr></table></figure><p>假设在代码中有对应的 <code>version</code> 和 <code>buildTime</code> 字符串变量，编译时会注入相应的值。</p><h4 id="保留调试信息但压缩文件"><a href="#保留调试信息但压缩文件" class="headerlink" title="保留调试信息但压缩文件"></a>保留调试信息但压缩文件</h4><p>如果你需要保留 DWARF 调试信息以便调试，但希望减少其大小，可以使用 <code>-compressdwarf</code>：<br>&#96;</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">go build -ldflags <span class="hljs-string">&quot;-compressdwarf&quot;</span><br></code></pre></td></tr></table></figure><h3 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h3><p>Go 编译选项中的 <code>-ldflags</code> 是一个强大且灵活的工具，它允许开发者根据项目需求进行定制化的编译优化。在生成生产环境二进制文件时，通过移除调试信息、注入动态变量等方式，可以显著减少文件体积、提高程序的安全性和便携性。</p><p>根据项目的实际需求，合理使用 <code>-ldflags</code> 参数，可以为你的 Go 项目提供更高的灵活性和优化能力。无论是减少二进制文件大小、动态注入版本信息，还是自定义链接器行为，<code>-ldflags</code> 都能帮助你更好地掌控编译过程。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/10/28/Go%E7%9A%84%E7%BC%96%E8%AF%91%E5%8F%82%E6%95%B0/</id>
    <link href="https://sunshinechen.cn/2025/10/28/Go%E7%9A%84%E7%BC%96%E8%AF%91%E5%8F%82%E6%95%B0/"/>
    <published>2025-10-28T06:14:50.000Z</published>
    <summary>在 Go 开发中，编译生成的二进制文件往往是程序性能和可移植性的关键。在某些场景下，我们需要对编译的输出进行优化，比如减少二进制文件大小、注入版本信息、控制调试信息等。而 -ldflags 是 Go 编译器提供的一个功能强大的选项，允许开发者向链接器传递额外的参数来控制生成文件的行为和结构。在本文中，我们将深入解析 Go 中的 -ldflags 及其常用参数，帮助开发者根据具体需求进行高效优化和自定义。</summary>
    <title>Go的编译参数</title>
    <updated>2026-10-28T09:18:36.382Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <category term="第三方库" scheme="https://sunshinechen.cn/tags/%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"/>
    <content>
      <![CDATA[<h2 id="程序库例子"><a href="#程序库例子" class="headerlink" title="程序库例子"></a>程序库例子</h2><h3 id="go-blueprint"><a href="#go-blueprint" class="headerlink" title="go-blueprint"></a>go-blueprint</h3><p><a href="https://github.com/Melkeydev/go-blueprint">go-blueprint</a>是一个快速生成Go项目文件夹的项目，提供项目名称、RESTFul框架、数据库驱动、git等多种技术栈选择，由<a href="https://github.com/charmbracelet/bubbletea">bubbletea TUI</a>文本框架来提供数据的录入。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2025/04/16/%E5%BA%93%E4%BE%8B%E5%AD%90/</id>
    <link href="https://sunshinechen.cn/2025/04/16/%E5%BA%93%E4%BE%8B%E5%AD%90/"/>
    <published>2025-04-16T06:17:13.000Z</published>
    <summary>
      <![CDATA[<h2 id="程序库例子"><a href="#程序库例子" class="headerlink" title="程序库例子"></a>程序库例子</h2><h3 id="go-blueprint"><a href="#go-blueprint" class="headerli]]>
    </summary>
    <title>库例子</title>
    <updated>2026-10-28T09:18:36.422Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="Ubuntu" scheme="https://sunshinechen.cn/tags/Ubuntu/"/>
    <content>
      <![CDATA[<p>在日常使用 Linux 系统时，不管是通过 SSH 远程登录设备，还是直接打开本地终端，当我们需要执行一些系统维护操作（比如更新软件包）时，都会遇到这样的情况：使用  <code>sudo</code>  命令后，系统会要求我们输入密码。</p><p>虽然这个设计出发点是安全考虑，但在个人使用场景下，频繁输入密码确实会影响工作效率。其实我们可以通过一个更优雅的方式来解决这个问题：在  <code>sudoers.d</code>  目录下创建独立的用户权限规则文件，而不是直接修改系统的默认配置。</p><p>只需要执行下面这条命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;`whoami` ALL=(ALL) NOPASSWD:ALL&quot;</span> | <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">tee</span> <span class="hljs-string">&quot;/etc/sudoers.d/dont-prompt-<span class="hljs-variable">$USER</span>-for-sudo-password&quot;</span><br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2025/02/11/%E6%89%A7%E8%A1%8C%E7%89%B9%E6%9D%83%E5%91%BD%E4%BB%A4%E6%97%B6%E4%B8%8D%E7%94%A8%E9%87%8D%E5%A4%8D%E8%BE%93%E5%85%A5%E5%AF%86%E7%A0%81/</id>
    <link href="https://sunshinechen.cn/2025/02/11/%E6%89%A7%E8%A1%8C%E7%89%B9%E6%9D%83%E5%91%BD%E4%BB%A4%E6%97%B6%E4%B8%8D%E7%94%A8%E9%87%8D%E5%A4%8D%E8%BE%93%E5%85%A5%E5%AF%86%E7%A0%81/"/>
    <published>2025-02-11T07:13:55.000Z</published>
    <summary>
      <![CDATA[<p>在日常使用 Linux 系统时，不管是通过 SSH 远程登录设备，还是直接打开本地终端，当我们需要执行一些系统维护操作（比如更新软件包）时，都会遇到这样的情况：使用  <code>sudo</code>  命令后，系统会要求我们输入密码。</p>
<p>虽然这个设计出发点是]]>
    </summary>
    <title>执行特权命令时不用重复输入密码</title>
    <updated>2026-10-28T09:18:36.438Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <category term="运维/Shell" scheme="https://sunshinechen.cn/tags/%E8%BF%90%E7%BB%B4-Shell/"/>
    <content>
      <![CDATA[<h3 id="获取当前脚本的目录"><a href="#获取当前脚本的目录" class="headerlink" title="获取当前脚本的目录"></a>获取当前脚本的目录</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">dir</span>=$(<span class="hljs-built_in">cd</span> $(<span class="hljs-built_in">dirname</span> <span class="hljs-variable">$0</span>);<span class="hljs-built_in">pwd</span>)<br></code></pre></td></tr></table></figure><h3 id="让输出有颜色"><a href="#让输出有颜色" class="headerlink" title="让输出有颜色"></a>让输出有颜色</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">BOLD_RED=<span class="hljs-string">&#x27;\033[1;31m&#x27;</span><br>RESET=<span class="hljs-string">&#x27;\033[0m&#x27;</span><br><span class="hljs-built_in">echo</span> -e <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;BOLD_RED&#125;</span>ERROR<span class="hljs-variable">$&#123;RESET&#125;</span>: ./calculate_average_<span class="hljs-variable">$fork</span>.sh does not exist.&quot;</span> &gt;&amp;2<br></code></pre></td></tr></table></figure><p>以上代码会让 <code>ERROR</code> 以红色字样输出，但后面的还是默认颜色的白色颜色。</p><h3 id="执行命令之前先打印命令本身"><a href="#执行命令之前先打印命令本身" class="headerlink" title="执行命令之前先打印命令本身"></a>执行命令之前先打印命令本身</h3><p>我希望用户能够准确理解脚本在每个步骤中的作用。为了实现这一点，我实现了一个函数，在执行每个命令之前打印它：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">print_and_execute</span></span>() &#123;<br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;+ <span class="hljs-variable">$@</span>&quot;</span> &gt;&amp;2<br>  <span class="hljs-string">&quot;<span class="hljs-variable">$@</span>&quot;</span><br>&#125;<br><br><span class="hljs-comment">## 调用时:</span><br>print_and_execute git add .<br></code></pre></td></tr></table></figure><h3 id="获取当前时分"><a href="#获取当前时分" class="headerlink" title="获取当前时分"></a>获取当前时分</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">date +&quot;%H:%M&quot;<br></code></pre></td></tr></table></figure><p>将实际爱你保存到变量中：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">current_time=$(<span class="hljs-built_in">date</span> +<span class="hljs-string">&quot;%H:%M&quot;</span>)<br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;当前时间是 <span class="hljs-variable">$current_time</span>&quot;</span><br></code></pre></td></tr></table></figure><p>其他常见时间格式：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">date +&quot;%I:%M %p&quot;  # 12 小时制 + AM/PM 标记（例如 02:30 PM）<br>date +&quot;%H%M&quot;      # 紧凑格式（例如 1430）<br></code></pre></td></tr></table></figure><p>指定时区：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">TZ=&#x27;Asia/Shanghai&#x27; date +&quot;%H:%M&quot;  # 获取上海时区的当前时间<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2024/11/13/%E7%BC%96%E5%86%99Shell%E8%84%9A%E6%9C%AC%E6%97%B6%E7%9A%84%E6%8A%80%E5%B7%A7/</id>
    <link href="https://sunshinechen.cn/2024/11/13/%E7%BC%96%E5%86%99Shell%E8%84%9A%E6%9C%AC%E6%97%B6%E7%9A%84%E6%8A%80%E5%B7%A7/"/>
    <published>2024-11-13T02:13:02.000Z</published>
    <summary>
      <![CDATA[<h3 id="获取当前脚本的目录"><a href="#获取当前脚本的目录" class="headerlink" title="获取当前脚本的目录"></a>获取当前脚本的目录</h3><figure class="highlight bash"><table><tr><td]]>
    </summary>
    <title>编写Shell脚本时的技巧</title>
    <updated>2026-10-28T09:18:36.290Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<h2 id="工具库"><a href="#工具库" class="headerlink" title="工具库"></a>工具库</h2><h3 id="Lancet"><a href="#Lancet" class="headerlink" title="Lancet"></a>Lancet</h3><p><a href="https://github.com/duke-git/lancet">github</a><br>全面的 Go 语言工具函数库。这是一个高效且全面的 Go 语言工具函数库，包含 600 多个函数，涵盖字符串处理、切片操作、网络编程、并发、加解密、文件处理、时间&#x2F;日期、流处理、迭代器等功能。</p><h3 id="gopsutil"><a href="#gopsutil" class="headerlink" title="gopsutil"></a>gopsutil</h3><p><a href="https://github.com/shirou/gopsutil?tab=readme-ov-file">github</a></p><p>用于系统监控的 go 库，可以监控 cpu、内存、硬盘、网络、进程、主机信息、docker 相关信息。</p><h3 id="fsm"><a href="#fsm" class="headerlink" title="fsm"></a>fsm</h3><p><a href="https://github.com/looplab/fsm">github</a></p><p>有限状态机在 Go 中的实现库</p><h3 id="fxjson"><a href="#fxjson" class="headerlink" title="fxjson"></a>fxjson</h3><p><a href="https://github.com/icloudza/fxjson">github</a> <a href="https://github.com/icloudza/fxjson/wiki/FxJSON-%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97%EF%BC%88%E4%B8%AD%E6%96%87%EF%BC%89">wiki</a></p><p>FxJSON 是一个专为 Go 语言设计的高性能 JSON 解析库，它让 JSON 处理变得快速、安全、简单。可以把它想象成标准 JSON 库的超级版本！</p><h2 id="Retry"><a href="#Retry" class="headerlink" title="Retry"></a>Retry</h2><p><a href="https://github.com/sethvargo/go-retry">github</a> <a href="https://juejin.cn/post/7574244766423007241">使用指南</a></p><p>Retry 是一个轻量纯 Go 实现的重试库，有丰富的重试策略、灵活的终止条件，贴合 Go 生态的设计，是 Go 语言中重试机制的首选库。</p>]]>
    </content>
    <id>https://sunshinechen.cn/2024/09/03/%E5%A5%BD%E7%94%A8%E7%9A%84%E4%B8%89%E6%96%B9%E5%BA%93/</id>
    <link href="https://sunshinechen.cn/2024/09/03/%E5%A5%BD%E7%94%A8%E7%9A%84%E4%B8%89%E6%96%B9%E5%BA%93/"/>
    <published>2024-09-03T09:42:34.000Z</published>
    <summary>
      <![CDATA[<h2 id="工具库"><a href="#工具库" class="headerlink" title="工具库"></a>工具库</h2><h3 id="Lancet"><a href="#Lancet" class="headerlink" title="Lancet"><]]>
    </summary>
    <title>好用的三方库</title>
    <updated>2026-10-28T09:18:36.414Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <content>
      <![CDATA[<h1 id="Systemd-入门教程：命令篇"><a href="#Systemd-入门教程：命令篇" class="headerlink" title="Systemd 入门教程：命令篇"></a>Systemd 入门教程：命令篇</h1><p>Systemd 是 Linux 系统工具，用来启动<a href="http://www.ruanyifeng.com/blog/2016/02/linux-daemon.html">守护进程</a>，已成为大多数发行版的标准配置。</p><p>本文介绍它的基本用法，分为上下两篇。今天介绍它的主要命令，下一篇介绍如何用于实战。</p><p><img src="http://www.ruanyifeng.com/blogimg/asset/2016/bg2016030701.gif"></p><h2 id="一、由来"><a href="#一、由来" class="headerlink" title="一、由来"></a>一、由来</h2><p>历史上，<a href="http://www.ruanyifeng.com/blog/2013/08/linux_boot_process.html">Linux 的启动</a>一直采用<a href="https://en.wikipedia.org/wiki/Init"><code>init</code></a>进程。</p><p>下面的命令用来启动服务。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> /etc/init.d/apache2 start<br><span class="hljs-comment"># 或者</span><br>$ service apache2 start<br></code></pre></td></tr></table></figure><p>这种方法有两个缺点。</p><p>一是启动时间长。<code>init</code>进程是串行启动，只有前一个进程启动完，才会启动下一个进程。</p><p>二是启动脚本复杂。<code>init</code>进程只是执行启动脚本，不管其他事情。脚本需要自己处理各种情况，这往往使得脚本变得很长。</p><h2 id="二、Systemd-概述"><a href="#二、Systemd-概述" class="headerlink" title="二、Systemd 概述"></a>二、Systemd 概述</h2><p>Systemd 就是为了解决这些问题而诞生的。它的设计目标是，为系统的启动和管理提供一套完整的解决方案。</p><p>根据 Linux 惯例，字母<code>d</code>是守护进程（daemon）的缩写。 Systemd 这个名字的含义，就是它要守护整个系统。</p><p><img src="http://www.ruanyifeng.com/blogimg/asset/2016/bg2016030702.jpg"></p><p>（上图为 Systemd 作者 <a href="https://en.wikipedia.org/wiki/Lennart_Poettering">Lennart Poettering</a>）</p><p>使用了 Systemd，就不需要再用<code>init</code>了。Systemd 取代了<code>initd</code>，成为系统的第一个进程（PID 等于 1），其他进程都是它的子进程。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl --version<br></code></pre></td></tr></table></figure><p>上面的命令查看 Systemd 的版本。</p><p>Systemd 的优点是功能强大，使用方便，缺点是系统庞大，非常复杂。事实上，现在还有很多人反对使用 Systemd，理由就是它过于复杂，与操作系统的其他部分强耦合，违反“keep simple, keep stupid”的<a href="http://www.ruanyifeng.com/blog/2009/06/unix_philosophy.html">Unix 哲学</a>。</p><p><img src="http://www.ruanyifeng.com/blogimg/asset/2016/bg2016030703.png"></p><p>（上图为 Systemd 架构图）</p><h2 id="三、系统管理"><a href="#三、系统管理" class="headerlink" title="三、系统管理"></a>三、系统管理</h2><p>Systemd 并不是一个命令，而是一组命令，涉及到系统管理的方方面面。</p><h3 id="3-1-systemctl"><a href="#3-1-systemctl" class="headerlink" title="3.1 systemctl"></a>3.1 systemctl</h3><p><code>systemctl</code>是 Systemd 的主命令，用于管理系统。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs bash"><span class="hljs-comment"># 重启系统</span><br>$ <span class="hljs-built_in">sudo</span> systemctl reboot<br><br><span class="hljs-comment"># 关闭系统，切断电源</span><br>$ <span class="hljs-built_in">sudo</span> systemctl poweroff<br><br><span class="hljs-comment"># CPU停止工作</span><br>$ <span class="hljs-built_in">sudo</span> systemctl halt<br><br><span class="hljs-comment"># 暂停系统</span><br>$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">suspend</span><br><br><span class="hljs-comment"># 让系统进入冬眠状态</span><br>$ <span class="hljs-built_in">sudo</span> systemctl hibernate<br><br><span class="hljs-comment"># 让系统进入交互式休眠状态</span><br>$ <span class="hljs-built_in">sudo</span> systemctl hybrid-sleep<br><br><span class="hljs-comment"># 启动进入救援状态（单用户状态）</span><br>$ <span class="hljs-built_in">sudo</span> systemctl rescue<br></code></pre></td></tr></table></figure><h3 id="3-2-systemd-analyze"><a href="#3-2-systemd-analyze" class="headerlink" title="3.2 systemd-analyze"></a>3.2 systemd-analyze</h3><p><code>systemd-analyze</code>命令用于查看启动耗时。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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><code class="hljs bash"><span class="hljs-comment"># 查看启动耗时</span><br>$ systemd-analyze                                                                                       <br><br><span class="hljs-comment"># 查看每个服务的启动耗时</span><br>$ systemd-analyze blame<br><br><span class="hljs-comment"># 显示瀑布状的启动过程流</span><br>$ systemd-analyze critical-chain<br><br><span class="hljs-comment"># 显示指定服务的启动流</span><br>$ systemd-analyze critical-chain atd.service<br></code></pre></td></tr></table></figure><h3 id="3-3-hostnamectl"><a href="#3-3-hostnamectl" class="headerlink" title="3.3 hostnamectl"></a>3.3 hostnamectl</h3><p><code>hostnamectl</code>命令用于查看当前主机的信息。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 显示当前主机的信息</span><br>$ hostnamectl<br><br><span class="hljs-comment"># 设置主机名。</span><br>$ <span class="hljs-built_in">sudo</span> hostnamectl set-hostname rhel7<br></code></pre></td></tr></table></figure><h3 id="3-4-localectl"><a href="#3-4-localectl" class="headerlink" title="3.4 localectl"></a>3.4 localectl</h3><p><code>localectl</code>命令用于查看本地化设置。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 查看本地化设置</span><br>$ localectl<br><br><span class="hljs-comment"># 设置本地化参数。</span><br>$ <span class="hljs-built_in">sudo</span> localectl set-locale LANG=en_GB.utf8<br>$ <span class="hljs-built_in">sudo</span> localectl set-keymap en_GB<br></code></pre></td></tr></table></figure><h3 id="3-5-timedatectl"><a href="#3-5-timedatectl" class="headerlink" title="3.5 timedatectl"></a>3.5 timedatectl</h3><p><code>timedatectl</code>命令用于查看当前时区设置。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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><code class="hljs bash"><span class="hljs-comment"># 查看当前时区设置</span><br>$ timedatectl<br><br><span class="hljs-comment"># 显示所有可用的时区</span><br>$ timedatectl list-timezones                                                                                   <br><br><span class="hljs-comment"># 设置当前时区</span><br>$ <span class="hljs-built_in">sudo</span> timedatectl set-timezone America/New_York<br>$ <span class="hljs-built_in">sudo</span> timedatectl set-time YYYY-MM-DD<br>$ <span class="hljs-built_in">sudo</span> timedatectl set-time HH:MM:SS<br></code></pre></td></tr></table></figure><h3 id="3-6-loginctl"><a href="#3-6-loginctl" class="headerlink" title="3.6 loginctl"></a>3.6 loginctl</h3><p><code>loginctl</code>命令用于查看当前登录的用户。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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><code class="hljs bash"><span class="hljs-comment"># 列出当前session</span><br>$ loginctl list-sessions<br><br><span class="hljs-comment"># 列出当前登录用户</span><br>$ loginctl list-users<br><br><span class="hljs-comment"># 列出显示指定用户的信息</span><br>$ loginctl show-user ruanyf<br></code></pre></td></tr></table></figure><h2 id="四、Unit"><a href="#四、Unit" class="headerlink" title="四、Unit"></a>四、Unit</h2><h3 id="4-1-含义"><a href="#4-1-含义" class="headerlink" title="4.1 含义"></a>4.1 含义</h3><p>Systemd 可以管理所有系统资源。不同的资源统称为 Unit（单位）。</p><p>Unit 一共分成12种。</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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><code class="hljs dart">- <span class="hljs-title class_">Service</span> unit：系统服务<br>- <span class="hljs-title class_">Target</span> unit：多个 <span class="hljs-title class_">Unit</span> 构成的一个组<br>- <span class="hljs-title class_">Device</span> <span class="hljs-title class_">Unit</span>：硬件设备<br>- <span class="hljs-title class_">Mount</span> <span class="hljs-title class_">Unit</span>：文件系统的挂载点<br>- <span class="hljs-title class_">Automount</span> <span class="hljs-title class_">Unit</span>：自动挂载点<br>- <span class="hljs-title class_">Path</span> <span class="hljs-title class_">Unit</span>：文件或路径<br>- <span class="hljs-title class_">Scope</span> <span class="hljs-title class_">Unit</span>：不是由 <span class="hljs-title class_">Systemd</span> 启动的外部进程<br>- <span class="hljs-title class_">Slice</span> <span class="hljs-title class_">Unit</span>：进程组<br>- <span class="hljs-title class_">Snapshot</span> <span class="hljs-title class_">Unit</span>：<span class="hljs-title class_">Systemd</span> 快照，可以切回某个快照<br>- <span class="hljs-title class_">Socket</span> <span class="hljs-title class_">Unit</span>：进程间通信的 socket<br>- <span class="hljs-title class_">Swap</span> <span class="hljs-title class_">Unit</span>：swap 文件<br>- <span class="hljs-title class_">Timer</span> <span class="hljs-title class_">Unit</span>：定时器<br></code></pre></td></tr></table></figure><p><code>systemctl list-units</code>命令可以查看当前系统的所有 Unit 。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 列出正在运行的 Unit</span><br>$ systemctl list-units<br><br><span class="hljs-comment"># 列出所有Unit，包括没有找到配置文件的或者启动失败的</span><br>$ systemctl list-units --all<br><br><span class="hljs-comment"># 列出所有没有运行的 Unit</span><br>$ systemctl list-units --all --state=inactive<br><br><span class="hljs-comment"># 列出所有加载失败的 Unit</span><br>$ systemctl list-units --failed<br><br><span class="hljs-comment"># 列出所有正在运行的、类型为 service 的 Unit</span><br>$ systemctl list-units --<span class="hljs-built_in">type</span>=service<br></code></pre></td></tr></table></figure><h3 id="4-2-Unit-的状态"><a href="#4-2-Unit-的状态" class="headerlink" title="4.2 Unit 的状态"></a>4.2 Unit 的状态</h3><p><code>systemctl status</code>命令用于查看系统状态和单个 Unit 的状态。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 显示系统状态</span><br>$ systemctl status<br><br><span class="hljs-comment"># 显示单个 Unit 的状态</span><br>$ sysystemctl status bluetooth.service<br><br><span class="hljs-comment"># 显示远程主机的某个 Unit 的状态</span><br>$ systemctl -H root@rhel7.example.com status httpd.service<br></code></pre></td></tr></table></figure><p>除了<code>status</code>命令，<code>systemctl</code>还提供了三个查询状态的简单方法，主要供脚本内部的判断语句使用。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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><code class="hljs bash"><span class="hljs-comment"># 显示某个 Unit 是否正在运行</span><br>$ systemctl is-active application.service<br><br><span class="hljs-comment"># 显示某个 Unit 是否处于启动失败状态</span><br>$ systemctl is-failed application.service<br><br><span class="hljs-comment"># 显示某个 Unit 服务是否建立了启动链接</span><br>$ systemctl is-enabled application.service<br></code></pre></td></tr></table></figure><h3 id="4-3-Unit-管理"><a href="#4-3-Unit-管理" class="headerlink" title="4.3 Unit 管理"></a>4.3 Unit 管理</h3><p>对于用户来说，最常用的是下面这些命令，用于启动和停止 Unit（主要是 service）。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 立即启动一个服务</span><br>$ <span class="hljs-built_in">sudo</span> systemctl start apache.service<br><br><span class="hljs-comment"># 立即停止一个服务</span><br>$ <span class="hljs-built_in">sudo</span> systemctl stop apache.service<br><br><span class="hljs-comment"># 重启一个服务</span><br>$ <span class="hljs-built_in">sudo</span> systemctl restart apache.service<br><br><span class="hljs-comment"># 杀死一个服务的所有子进程</span><br>$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">kill</span> apache.service<br><br><span class="hljs-comment"># 重新加载一个服务的配置文件</span><br>$ <span class="hljs-built_in">sudo</span> systemctl reload apache.service<br><br><span class="hljs-comment"># 重载所有修改过的配置文件</span><br>$ <span class="hljs-built_in">sudo</span> systemctl daemon-reload<br><br><span class="hljs-comment"># 显示某个 Unit 的所有底层参数</span><br>$ systemctl show httpd.service<br><br><span class="hljs-comment"># 显示某个 Unit 的指定属性的值</span><br>$ systemctl show -p CPUShares httpd.service<br><br><span class="hljs-comment"># 设置某个 Unit 的指定属性</span><br>$ <span class="hljs-built_in">sudo</span> systemctl set-property httpd.service CPUShares=500<br></code></pre></td></tr></table></figure><h3 id="4-4-依赖关系"><a href="#4-4-依赖关系" class="headerlink" title="4.4 依赖关系"></a>4.4 依赖关系</h3><p>Unit 之间存在依赖关系：A 依赖于 B，就意味着 Systemd 在启动 A 的时候，同时会去启动 B。</p><p><code>systemctl list-dependencies</code>命令列出一个 Unit 的所有依赖。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl list-dependencies nginx.service<br></code></pre></td></tr></table></figure><p>上面命令的输出结果之中，有些依赖是 Target 类型（详见下文），默认不会展开显示。如果要展开 Target，就需要使用<code>--all</code>参数。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl list-dependencies --all nginx.service<br></code></pre></td></tr></table></figure><h2 id="五、Unit-的配置文件"><a href="#五、Unit-的配置文件" class="headerlink" title="五、Unit 的配置文件"></a>五、Unit 的配置文件</h2><h3 id="5-1-概述"><a href="#5-1-概述" class="headerlink" title="5.1 概述"></a>5.1 概述</h3><p>每一个 Unit 都有一个配置文件，告诉 Systemd 怎么启动这个 Unit 。</p><p>Systemd 默认从目录<code>/etc/systemd/system/</code>读取配置文件。但是，里面存放的大部分文件都是符号链接，指向目录<code>/usr/lib/systemd/system/</code>，真正的配置文件存放在那个目录。</p><p><code>systemctl enable</code>命令用于在上面两个目录之间，建立符号链接关系。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">enable</span> clamd@scan.service<br><span class="hljs-comment"># 等同于</span><br>$ <span class="hljs-built_in">sudo</span> <span class="hljs-built_in">ln</span> -s <span class="hljs-string">&#x27;/usr/lib/systemd/system/clamd@scan.service&#x27;</span> <span class="hljs-string">&#x27;/etc/systemd/system/multi-user.target.wants/clamd@scan.service&#x27;</span><br></code></pre></td></tr></table></figure><p>如果配置文件里面设置了开机启动，<code>systemctl enable</code>命令相当于激活开机启动。</p><p>与之对应的，<code>systemctl disable</code>命令用于在两个目录之间，撤销符号链接关系，相当于撤销开机启动。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">disable</span> clamd@scan.service<br></code></pre></td></tr></table></figure><p>配置文件的后缀名，就是该 Unit 的种类，比如<code>sshd.socket</code>。如果省略，Systemd 默认后缀名为<code>.service</code>，所以<code>sshd</code>会被理解成<code>sshd.service</code>。</p><h3 id="5-2-配置文件的状态"><a href="#5-2-配置文件的状态" class="headerlink" title="5.2 配置文件的状态"></a>5.2 配置文件的状态</h3><p><code>systemctl list-unit-files</code>命令用于列出所有配置文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 列出所有配置文件</span><br>$ systemctl list-unit-files<br><br><span class="hljs-comment"># 列出指定类型的配置文件</span><br>$ systemctl list-unit-files --<span class="hljs-built_in">type</span>=service<br></code></pre></td></tr></table></figure><p>这个命令会输出一个列表。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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><code class="hljs bash">$ systemctl list-unit-files<br><br>UNIT FILE              STATE<br>chronyd.service        enabled<br>clamd@.service         static<br>clamd@scan.service     disabled<br></code></pre></td></tr></table></figure><p>这个列表显示每个配置文件的状态，一共有四种。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs markdown"><span class="hljs-bullet">-</span> enabled：已建立启动链接<br><span class="hljs-bullet">-</span> disabled：没建立启动链接<br><span class="hljs-bullet">-</span> static：该配置文件没有<span class="hljs-code">`[Install]`</span>部分（无法执行），只能作为其他配置文件的依赖<br><span class="hljs-bullet">-</span> masked：该配置文件被禁止建立启动链接<br></code></pre></td></tr></table></figure><p>注意，从配置文件的状态无法看出，该 Unit 是否正在运行。这必须执行前面提到的<code>systemctl status</code>命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl status bluetooth.service<br></code></pre></td></tr></table></figure><p>一旦修改配置文件，就要让 SystemD 重新加载配置文件，然后重新启动，否则修改不会生效。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl daemon-reload<br>$ <span class="hljs-built_in">sudo</span> systemctl restart httpd.service<br></code></pre></td></tr></table></figure><h3 id="5-3-配置文件的格式"><a href="#5-3-配置文件的格式" class="headerlink" title="5.3 配置文件的格式"></a>5.3 配置文件的格式</h3><p>配置文件就是普通的文本文件，可以用文本编辑器打开。</p><p><code>systemctl cat</code>命令可以查看配置文件的内容。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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><code class="hljs bash">$ systemctl <span class="hljs-built_in">cat</span> atd.service<br><br>[Unit]<br>Description=ATD daemon<br><br>[Service]<br>Type=forking<br>ExecStart=/usr/bin/atd<br><br>[Install]<br>WantedBy=multi-user.target<br></code></pre></td></tr></table></figure><p>从上面的输出可以看到，配置文件分成几个区块。每个区块的第一行，是用方括号表示的区别名，比如<code>[Unit]</code>。注意，配置文件的区块名和字段名，都是大小写敏感的。</p><p>每个区块内部是一些等号连接的键值对。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash">[Section]<br>Directive1=value<br>Directive2=value<br><br>. . .<br></code></pre></td></tr></table></figure><p>注意，键值对的等号两侧不能有空格。</p><h3 id="5-4-配置文件的区块"><a href="#5-4-配置文件的区块" class="headerlink" title="5.4 配置文件的区块"></a>5.4 配置文件的区块</h3><p><code>[Unit]</code>区块通常是配置文件的第一个区块，用来定义 Unit 的元数据，以及配置与其他 Unit 的关系。它的主要字段如下。</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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><code class="hljs dart">- <span class="hljs-title class_">Description</span>：简短描述<br>- <span class="hljs-title class_">Documentation</span>：文档地址<br>- <span class="hljs-title class_">Requires</span>：当前 <span class="hljs-title class_">Unit</span> 依赖的其他 <span class="hljs-title class_">Unit</span>，如果它们没有运行，当前 <span class="hljs-title class_">Unit</span> 会启动失败<br>- <span class="hljs-title class_">Wants</span>：与当前 <span class="hljs-title class_">Unit</span> 配合的其他 <span class="hljs-title class_">Unit</span>，如果它们没有运行，当前 <span class="hljs-title class_">Unit</span> 不会启动失败<br>- <span class="hljs-title class_">BindsTo</span>：与`<span class="hljs-title class_">Requires</span>`类似，它指定的 <span class="hljs-title class_">Unit</span> 如果退出，会导致当前 <span class="hljs-title class_">Unit</span> 停止运行<br>- <span class="hljs-title class_">Before</span>：如果该字段指定的 <span class="hljs-title class_">Unit</span> 也要启动，那么必须在当前 <span class="hljs-title class_">Unit</span> 之后启动<br>- <span class="hljs-title class_">After</span>：如果该字段指定的 <span class="hljs-title class_">Unit</span> 也要启动，那么必须在当前 <span class="hljs-title class_">Unit</span> 之前启动<br>- <span class="hljs-title class_">Conflicts</span>：这里指定的 <span class="hljs-title class_">Unit</span> 不能与当前 <span class="hljs-title class_">Unit</span> 同时运行<br>- <span class="hljs-title class_">Condition</span>...：当前 <span class="hljs-title class_">Unit</span> 运行必须满足的条件，否则不会运行<br>- <span class="hljs-title class_">Assert</span>...：当前 <span class="hljs-title class_">Unit</span> 运行必须满足的条件，否则会报启动失败<br></code></pre></td></tr></table></figure><p><code>[Install]</code>通常是配置文件的最后一个区块，用来定义如何启动，以及是否开机启动。它的主要字段如下。</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><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><code class="hljs dart">- <span class="hljs-title class_">WantedBy</span>：它的值是一个或多个 <span class="hljs-title class_">Target</span>，当前 <span class="hljs-title class_">Unit</span> 激活时（enable）符号链接会放入`/etc/systemd/system`目录下面以 <span class="hljs-title class_">Target</span> 名 + `.wants`后缀构成的子目录中<br>- <span class="hljs-title class_">RequiredBy</span>：它的值是一个或多个 <span class="hljs-title class_">Target</span>，当前 <span class="hljs-title class_">Unit</span> 激活时，符号链接会放入`/etc/systemd/system`目录下面以 <span class="hljs-title class_">Target</span> 名 + `.<span class="hljs-keyword">required</span>`后缀构成的子目录中<br>- <span class="hljs-title class_">Alias</span>：当前 <span class="hljs-title class_">Unit</span> 可用于启动的别名<br>- <span class="hljs-title class_">Also</span>：当前 <span class="hljs-title class_">Unit</span> 激活（enable）时，会被同时激活的其他 <span class="hljs-title class_">Unit</span><br></code></pre></td></tr></table></figure><p><code>[Service]</code>区块用来 Service 的配置，只有 Service 类型的 Unit 才有这个区块。它的主要字段如下。</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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><code class="hljs dart">- <span class="hljs-title class_">Type</span>：定义启动时的进程行为。它有以下几种值。<br>- simple：默认值，执行<span class="hljs-title class_">ExecStart</span>指定的命令，启动主进程<br>- forking：以 fork 方式从父进程创建子进程，创建后父进程会立即退出<br>- oneshot：一次性进程，<span class="hljs-title class_">Systemd</span> 会等当前服务退出，再继续往下执行<br>- dbus：当前服务通过D-<span class="hljs-title class_">Bus</span>启动<br>- notify：当前服务启动完毕，会通知<span class="hljs-title class_">Systemd</span>，再继续往下执行<br>- idle：若有其他任务执行完毕，当前服务才会运行<br>- <span class="hljs-title class_">ExecStart</span>：启动当前服务的命令<br>- <span class="hljs-title class_">ExecStartPre</span>：启动当前服务之前执行的命令<br>- <span class="hljs-title class_">ExecStartPost</span>：启动当前服务之后执行的命令<br>- <span class="hljs-title class_">ExecReload</span>：重启当前服务时执行的命令<br>- <span class="hljs-title class_">ExecStop</span>：停止当前服务时执行的命令<br>- <span class="hljs-title class_">ExecStopPost</span>：停止当其服务之后执行的命令<br>- <span class="hljs-title class_">RestartSec</span>：自动重启当前服务间隔的秒数<br>- <span class="hljs-title class_">Restart</span>：定义何种情况 <span class="hljs-title class_">Systemd</span> 会自动重启当前服务，可能的值包括always（总是重启）、<span class="hljs-keyword">on</span>-success、<span class="hljs-keyword">on</span>-failure、<span class="hljs-keyword">on</span>-abnormal、<span class="hljs-keyword">on</span>-abort、<span class="hljs-keyword">on</span>-watchdog<br>- <span class="hljs-title class_">TimeoutSec</span>：定义 <span class="hljs-title class_">Systemd</span> 停止当前服务之前等待的秒数<br>- <span class="hljs-title class_">Environment</span>：指定环境变量<br></code></pre></td></tr></table></figure><p>Unit 配置文件的完整字段清单，请参考<a href="https://www.freedesktop.org/software/systemd/man/systemd.unit.html">官方文档</a>。</p><h2 id="六、Target"><a href="#六、Target" class="headerlink" title="六、Target"></a>六、Target</h2><p>简单说，Target 就是一个 Unit 组，包含许多相关的 Unit 。启动某个 Target 的时候，Systemd 就会启动里面所有的 Unit。从这个意义上说，Target 这个概念类似于“状态点”，启动某个 Target 就好比启动到某种状态。</p><p>传统的<code>init</code>启动模式里面，有 RunLevel 的概念，跟 Target 的作用很类似。不同的是，RunLevel 是互斥的，不可能多个 RunLevel 同时启动，但是多个 Target 可以同时启动。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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><code class="hljs bash"><span class="hljs-comment"># 查看当前系统的所有 Target</span><br>$ systemctl list-unit-files --<span class="hljs-built_in">type</span>=target<br><br><span class="hljs-comment"># 查看一个 Target 包含的所有 Unit</span><br>$ systemctl list-dependencies multi-user.target<br><br><span class="hljs-comment"># 查看启动时的默认 Target</span><br>$ systemctl get-default<br><br><span class="hljs-comment"># 设置启动时的默认 Target</span><br>$ <span class="hljs-built_in">sudo</span> systemctl set-default multi-user.target<br><br><span class="hljs-comment"># 切换 Target 时，默认不关闭前一个 Target 启动的进程，</span><br><span class="hljs-comment"># systemctl isolate 命令改变这种行为，</span><br><span class="hljs-comment"># 关闭前一个 Target 里面所有不属于后一个 Target 的进程</span><br>$ <span class="hljs-built_in">sudo</span> systemctl isolate multi-user.target<br></code></pre></td></tr></table></figure><p>Target 与 传统 RunLevel 的对应关系如下。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash">Traditional runlevel      New target name     Symbolically linked to...<br><br>Runlevel 0           |    runlevel0.target -&gt; poweroff.target<br>Runlevel 1           |    runlevel1.target -&gt; rescue.target<br>Runlevel 2           |    runlevel2.target -&gt; multi-user.target<br>Runlevel 3           |    runlevel3.target -&gt; multi-user.target<br>Runlevel 4           |    runlevel4.target -&gt; multi-user.target<br>Runlevel 5           |    runlevel5.target -&gt; graphical.target<br>Runlevel 6           |    runlevel6.target -&gt; reboot.target<br></code></pre></td></tr></table></figure><p>它与<code>init</code>进程的主要差别如下。</p><blockquote><p><strong>（1）默认的 RunLevel</strong>（在<code>/etc/inittab</code>文件设置）现在被默认的 Target 取代，位置是<code>/etc/systemd/system/default.target</code>，通常符号链接到<code>graphical.target</code>（图形界面）或者<code>multi-user.target</code>（多用户命令行）。</p><p><strong>（2）启动脚本的位置</strong>，以前是<code>/etc/init.d</code>目录，符号链接到不同的 RunLevel 目录 （比如<code>/etc/rc3.d</code>、<code>/etc/rc5.d</code>等），现在则存放在<code>/lib/systemd/system</code>和<code>/etc/systemd/system</code>目录。</p><p><strong>（3）配置文件的位置</strong>，以前<code>init</code>进程的配置文件是<code>/etc/inittab</code>，各种服务的配置文件存放在<code>/etc/sysconfig</code>目录。现在的配置文件主要存放在<code>/lib/systemd</code>目录，在<code>/etc/systemd</code>目录里面的修改可以覆盖原始设置。</p></blockquote><h2 id="七、日志管理"><a href="#七、日志管理" class="headerlink" title="七、日志管理"></a>七、日志管理</h2><p>Systemd 统一管理所有 Unit 的启动日志。带来的好处就是，可以只用<code>journalctl</code>一个命令，查看所有日志（内核日志和应用日志）。日志的配置文件是<code>/etc/systemd/journald.conf</code>。</p><p><code>journalctl</code>功能强大，用法非常多。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 查看所有日志（默认情况下 ，只保存本次启动的日志）</span><br>$ <span class="hljs-built_in">sudo</span> journalctl<br><br><span class="hljs-comment"># 查看内核日志（不显示应用日志）</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -k<br><br><span class="hljs-comment"># 查看系统本次启动的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -b<br>$ <span class="hljs-built_in">sudo</span> journalctl -b -0<br><br><span class="hljs-comment"># 查看上一次启动的日志（需更改设置）</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -b -1<br><br><span class="hljs-comment"># 查看指定时间的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --since=<span class="hljs-string">&quot;2012-10-30 18:17:16&quot;</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --since <span class="hljs-string">&quot;20 min ago&quot;</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --since yesterday<br>$ <span class="hljs-built_in">sudo</span> journalctl --since <span class="hljs-string">&quot;2015-01-10&quot;</span> --<span class="hljs-keyword">until</span> <span class="hljs-string">&quot;2015-01-11 03:00&quot;</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --since 09:00 --<span class="hljs-keyword">until</span> <span class="hljs-string">&quot;1 hour ago&quot;</span><br><br><span class="hljs-comment"># 显示尾部的最新10行日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -n<br><br><span class="hljs-comment"># 显示尾部指定行数的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -n 20<br><br><span class="hljs-comment"># 实时滚动显示最新日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -f<br><br><span class="hljs-comment"># 查看指定服务的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl /usr/lib/systemd/systemd<br><br><span class="hljs-comment"># 查看指定进程的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl _PID=1<br><br><span class="hljs-comment"># 查看某个路径的脚本的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl /usr/bin/bash<br><br><span class="hljs-comment"># 查看指定用户的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl _UID=33 --since today<br><br><span class="hljs-comment"># 查看某个 Unit 的日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -u nginx.service<br>$ <span class="hljs-built_in">sudo</span> journalctl -u nginx.service --since today<br><br><span class="hljs-comment"># 实时滚动显示某个 Unit 的最新日志</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -u nginx.service -f<br><br><span class="hljs-comment"># 合并显示多个 Unit 的日志</span><br>$ journalctl -u nginx.service -u php-fpm.service --since today<br><br><span class="hljs-comment"># 查看指定优先级（及其以上级别）的日志，共有8级</span><br><span class="hljs-comment"># 0: emerg</span><br><span class="hljs-comment"># 1: alert</span><br><span class="hljs-comment"># 2: crit</span><br><span class="hljs-comment"># 3: err</span><br><span class="hljs-comment"># 4: warning</span><br><span class="hljs-comment"># 5: notice</span><br><span class="hljs-comment"># 6: info</span><br><span class="hljs-comment"># 7: debug</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -p err -b<br><br><span class="hljs-comment"># 日志默认分页输出，--no-pager 改为正常的标准输出</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --no-pager<br><br><span class="hljs-comment"># 以 JSON 格式（单行）输出</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -b -u nginx.service -o json<br><br><span class="hljs-comment"># 以 JSON 格式（多行）输出，可读性更好</span><br>$ <span class="hljs-built_in">sudo</span> journalctl -b -u nginx.serviceqq<br> -o json-pretty<br><br><span class="hljs-comment"># 显示日志占据的硬盘空间</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --disk-usage<br><br><span class="hljs-comment"># 指定日志文件占据的最大空间</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --vacuum-size=1G<br><br><span class="hljs-comment"># 指定日志文件保存多久</span><br>$ <span class="hljs-built_in">sudo</span> journalctl --vacuum-time=1years<br></code></pre></td></tr></table></figure><p>（完）</p><p>如何编写 Unit 的配置文件</p><ul><li><a href="https://fedoramagazine.org/what-is-an-init-system/">What is an init system?</a></li><li><a href="http://fedoramagazine.org/systemd-converting-sysvinit-scripts/">systemd: Converting sysvinit scripts</a></li><li><a href="https://fedoramagazine.org/systemd-unit-dependencies-and-order/">systemd: Unit dependencies and order</a></li><li><a href="https://fedoraproject.org/wiki/Systemd">Systemd</a></li><li><a href="http://fedoraproject.org/wiki/Packaging%3aSystemd">Packaging:Systemd</a></li><li><a href="http://patrakov.blogspot.jp/2011/01/writing-systemd-service-files.html">Writing systemd service files</a></li><li><a href="http://unix.stackexchange.com/questions/47695/how-to-write-startup-script-for-systemd">How to write startup script for systemd</a></li></ul>]]>
    </content>
    <id>https://sunshinechen.cn/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%881%EF%BC%89/</id>
    <link href="https://sunshinechen.cn/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%881%EF%BC%89/"/>
    <published>2024-08-21T09:07:35.000Z</published>
    <summary>
      <![CDATA[<h1 id="Systemd-入门教程：命令篇"><a href="#Systemd-入门教程：命令篇" class="headerlink" title="Systemd 入门教程：命令篇"></a>Systemd 入门教程：命令篇</h1><p>Systemd 是 Linu]]>
    </summary>
    <title>Systemd 入门教程：命令篇</title>
    <updated>2026-10-28T09:18:36.270Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="Linux" scheme="https://sunshinechen.cn/categories/Linux/"/>
    <content>
      <![CDATA[<h1 id="Systemd-入门教程：实战篇"><a href="#Systemd-入门教程：实战篇" class="headerlink" title="Systemd 入门教程：实战篇"></a>Systemd 入门教程：实战篇</h1><p>上一篇文章，我介绍了 Systemd 的<a href="http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html">主要命令</a>，今天介绍如何使用它完成一些基本的任务。</p><p><img src="http://www.ruanyifeng.com/blogimg/asset/2016/bg2016030801.jpg"></p><h2 id="一、开机启动"><a href="#一、开机启动" class="headerlink" title="一、开机启动"></a>一、开机启动</h2><p>对于那些支持 Systemd 的软件，安装的时候，会自动在<code>/usr/lib/systemd/system</code>目录添加一个配置文件。</p><p>如果你想让该软件开机启动，就执行下面的命令（以<code>httpd.service</code>为例）。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">enable</span> httpd<br></code></pre></td></tr></table></figure><p>上面的命令相当于在<code>/etc/systemd/system</code>目录添加一个符号链接，指向<code>/usr/lib/systemd/system</code>里面的<code>httpd.service</code>文件。</p><p>这是因为开机时，<code>Systemd</code>只执行<code>/etc/systemd/system</code>目录里面的配置文件。这也意味着，如果把修改后的配置文件放在该目录，就可以达到覆盖原始配置的效果。</p><h2 id="二、启动服务"><a href="#二、启动服务" class="headerlink" title="二、启动服务"></a>二、启动服务</h2><p>设置开机启动以后，软件并不会立即启动，必须等到下一次开机。如果想现在就运行该软件，那么要执行<code>systemctl start</code>命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl start httpd<br></code></pre></td></tr></table></figure><p>执行上面的命令以后，有可能启动失败，因此要用<code>systemctl statuc</code>命令查看一下该服务的状态。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl status httpd<br><br>httpd.service - The Apache HTTP Server<br>   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled)<br>   Active: active (running) since 金 2014-12-05 12:18:22 JST; 7min ago<br> Main PID: 4349 (httpd)<br>   Status: <span class="hljs-string">&quot;Total requests: 1; Current requests/sec: 0; Current traffic:   0 B/sec&quot;</span><br>   CGroup: /system.slice/httpd.service<br>           ├─4349 /usr/sbin/httpd -DFOREGROUND<br>           ├─4350 /usr/sbin/httpd -DFOREGROUND<br>           ├─4351 /usr/sbin/httpd -DFOREGROUND<br>           ├─4352 /usr/sbin/httpd -DFOREGROUND<br>           ├─4353 /usr/sbin/httpd -DFOREGROUND<br>           └─4354 /usr/sbin/httpd -DFOREGROUND<br><br>12月 05 12:18:22 localhost.localdomain systemd[1]: Starting The Apache HTTP Server...<br>12月 05 12:18:22 localhost.localdomain systemd[1]: Started The Apache HTTP Server.<br>12月 05 12:22:40 localhost.localdomain systemd[1]: Started The Apache HTTP Server.<br></code></pre></td></tr></table></figure><p>上面的输出结果含义如下。</p><blockquote><ul><li><code>Loaded</code>行：配置文件的位置，是否设为开机启动</li></ul></blockquote><ul><li><code>Active</code>行：表示正在运行</li><li><code>Main PID</code>行：主进程ID</li><li><code>Status</code>行：由应用本身（这里是 httpd ）提供的软件当前状态</li><li><code>CGroup</code>块：应用的所有子进程</li><li>日志块：应用的日志</li></ul><h2 id="三、停止服务"><a href="#三、停止服务" class="headerlink" title="三、停止服务"></a>三、停止服务</h2><p>终止正在运行的服务，需要执行<code>systemctl stop</code>命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl stop httpd.service<br></code></pre></td></tr></table></figure><p>有时候，该命令可能没有响应，服务停不下来。这时候就不得不“杀进程”了，向正在运行的进程发出<code>kill</code>信号。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl <span class="hljs-built_in">kill</span> httpd.service<br></code></pre></td></tr></table></figure><p>此外，重启服务要执行<code>systemctl restart</code>命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ <span class="hljs-built_in">sudo</span> systemctl restart httpd.service<br></code></pre></td></tr></table></figure><h2 id="四、读懂配置文件"><a href="#四、读懂配置文件" class="headerlink" title="四、读懂配置文件"></a>四、读懂配置文件</h2><p>一个服务怎么启动，完全由它的配置文件决定。下面就来看，配置文件有些什么内容。</p><p>前面说过，配置文件主要放在<code>/usr/lib/systemd/system</code>目录，也可能在<code>/usr/lib/systemd/system</code>目录。找到配置文件以后，使用文本编辑器打开即可。</p><p><code>systemctl cat</code>命令可以用来查看配置文件，下面以<code>sshd.service</code>文件为例，它的作用是启动一个 SSH 服务器，供其他用户以 SSH 方式登录。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs bash">$ systemctl <span class="hljs-built_in">cat</span> sshd.service<br><br>[Unit]<br>Description=OpenSSH server daemon<br>Documentation=man:sshd(8) man:sshd_config(5)<br>After=network.target sshd-keygen.service<br>Wants=sshd-keygen.service<br><br>[Service]<br>EnvironmentFile=/etc/sysconfig/sshd<br>ExecStart=/usr/sbin/sshd -D <span class="hljs-variable">$OPTIONS</span><br>ExecReload=/bin/kill -HUP <span class="hljs-variable">$MAINPID</span><br>Type=simple<br>KillMode=process<br>Restart=on-failure<br>RestartSec=42s<br><br>[Install]<br>WantedBy=multi-user.target<br></code></pre></td></tr></table></figure><p>可以看到，配置文件分成几个区块，每个区块包含若干条键值对。</p><p>下面依次解释每个区块的内容。</p><h2 id="五、-Unit-区块：启动顺序与依赖关系。"><a href="#五、-Unit-区块：启动顺序与依赖关系。" class="headerlink" title="五、 [Unit] 区块：启动顺序与依赖关系。"></a>五、 [Unit] 区块：启动顺序与依赖关系。</h2><p><code>Unit</code>区块的<code>Description</code>给出当前服务的简单描述，<code>Documentation</code>字段给出文档位置。</p><p>接下来的设置是启动顺序和依赖关系，这个比较重要。</p><blockquote><p><code>After</code>字段：表示如果<code>network.target</code>或<code>sshd-keygen.service</code>需要启动，那么<code>sshd.service</code>应该在它们之后启动。</p></blockquote><p>相应地，还有一个<code>Before</code>字段，定义<code>sshd.service</code>应该在哪些服务之前启动。</p><p>注意，<code>After</code>和<code>Before</code>字段只涉及启动顺序，不涉及依赖关系。</p><p>举例来说，某 Web 应用需要 postgresql 数据库储存数据。在配置文件中，它只定义要在 postgresql 之后启动，而没有定义依赖 postgresql 。上线后，由于某种原因，postgresql 需要重新启动，在停止服务期间，该 Web 应用就会无法建立数据库连接。</p><p>设置依赖关系，需要使用<code>Wants</code>字段和<code>Requires</code>字段。</p><blockquote><p><code>Wants</code>字段：表示<code>sshd.service</code>与<code>sshd-keygen.service</code>之间存在“弱依赖”关系，即如果“sshd-keygen.service”启动失败或停止运行，不影响<code>sshd.service</code>继续执行。</p></blockquote><p><code>Requires</code>字段则表示“强依赖”关系，即如果该服务启动失败或异常退出，那么<code>sshd.service</code>也必须退出。</p><p>注意，<code>Wants</code>字段与<code>Requires</code>字段只涉及依赖关系，与启动顺序无关，默认情况下是同时启动的。</p><h2 id="六、-Service-区块：启动行为"><a href="#六、-Service-区块：启动行为" class="headerlink" title="六、[Service] 区块：启动行为"></a>六、[Service] 区块：启动行为</h2><p><code>Service</code>区块定义如何启动当前服务。</p><h3 id="6-1-启动命令"><a href="#6-1-启动命令" class="headerlink" title="6.1 启动命令"></a>6.1 启动命令</h3><p>许多软件都有自己的环境参数文件，该文件可以用<code>EnvironmentFile</code>字段读取。</p><blockquote><p><code>EnvironmentFile</code>字段：指定当前服务的环境参数文件。该文件内部的<code>key=value</code>键值对，可以用<code>$key</code>的形式，在当前配置文件中获取。</p></blockquote><p>上面的例子中，sshd 的环境参数文件是<code>/etc/sysconfig/sshd</code>。</p><p>配置文件里面最重要的字段是<code>ExecStart</code>。</p><blockquote><p><code>ExecStart</code>字段：定义启动进程时执行的命令。</p></blockquote><p>上面的例子中，启动<code>sshd</code>，执行的命令是<code>/usr/sbin/sshd -D $OPTIONS</code>，其中的变量<code>$OPTIONS</code>就来自<code>EnvironmentFile</code>字段指定的环境参数文件。</p><p>与之作用相似的，还有如下这些字段。</p><blockquote><ul><li><code>ExecReload</code>字段：重启服务时执行的命令</li><li><code>ExecStop</code>字段：停止服务时执行的命令</li><li><code>ExecStartPre</code>字段：启动服务之前执行的命令</li><li><code>ExecStartPost</code>字段：启动服务之后执行的命令</li><li><code>ExecStopPost</code>字段：停止服务之后执行的命令</li></ul></blockquote><p>请看下面的例子。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash">[Service]<br>ExecStart=/bin/echo execstart1<br>ExecStart=<br>ExecStart=/bin/echo execstart2<br>ExecStartPost=/bin/echo post1<br>ExecStartPost=/bin/echo post2<br></code></pre></td></tr></table></figure><p>上面这个配置文件，第二行<code>ExecStart</code>设为空值，等于取消了第一行的设置，运行结果如下。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">execstart2<br>post1<br>post2<br></code></pre></td></tr></table></figure><p>所有的启动设置之前，都可以加上一个连词号（<code>-</code>），表示“抑制错误”，即发生错误的时候，不影响其他命令的执行。比如，<code>EnvironmentFile=-/etc/sysconfig/sshd</code>（注意等号后面的那个连词号），就表示即使<code>/etc/sysconfig/sshd</code>文件不存在，也不会抛出错误。</p><h3 id="6-2-启动类型"><a href="#6-2-启动类型" class="headerlink" title="6.2 启动类型"></a>6.2 启动类型</h3><p><code>Type</code>字段定义启动类型。它可以设置的值如下。</p><blockquote><ul><li>simple（默认值）：<code>ExecStart</code>字段启动的进程为主进程</li></ul></blockquote><ul><li>forking：<code>ExecStart</code>字段将以<code>fork()</code>方式启动，此时父进程将会退出，子进程将成为主进程</li><li>oneshot：类似于<code>simple</code>，但只执行一次，Systemd 会等它执行完，才启动其他服务</li><li>dbus：类似于<code>simple</code>，但会等待 D-Bus 信号后启动</li><li>notify：类似于<code>simple</code>，启动结束后会发出通知信号，然后 Systemd 再启动其他服务</li><li>idle：类似于<code>simple</code>，但是要等到其他任务都执行完，才会启动该服务。一种使用场合是为让该服务的输出，不与其他服务的输出相混合</li></ul><p>下面是一个<code>oneshot</code>的例子，笔记本电脑启动时，要把触摸板关掉，配置文件可以这样写。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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><code class="hljs bash">[Unit]<br>Description=Switch-off Touchpad<br><br>[Service]<br>Type=oneshot<br>ExecStart=/usr/bin/touchpad-off<br><br>[Install]<br>WantedBy=multi-user.target<br></code></pre></td></tr></table></figure><p>上面的配置文件，启动类型设为<code>oneshot</code>，就表明这个服务只要运行一次就够了，不需要长期运行。</p><p>如果关闭以后，将来某个时候还想打开，配置文件修改如下。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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><code class="hljs bash">[Unit]<br>Description=Switch-off Touchpad<br><br>[Service]<br>Type=oneshot<br>ExecStart=/usr/bin/touchpad-off start<br>ExecStop=/usr/bin/touchpad-off stop<br>RemainAfterExit=<span class="hljs-built_in">yes</span><br><br>[Install]<br>WantedBy=multi-user.target<br></code></pre></td></tr></table></figure><p>上面配置文件中，<code>RemainAfterExit</code>字段设为<code>yes</code>，表示进程退出以后，服务仍然保持执行。这样的话，一旦使用<code>systemctl stop</code>命令停止服务，<code>ExecStop</code>指定的命令就会执行，从而重新开启触摸板。</p><h3 id="6-3-重启行为"><a href="#6-3-重启行为" class="headerlink" title="6.3 重启行为"></a>6.3 重启行为</h3><p><code>Service</code>区块有一些字段，定义了重启行为。</p><blockquote><p><code>KillMode</code>字段：定义 Systemd 如何停止 sshd 服务。</p></blockquote><p>上面这个例子中，将<code>KillMode</code>设为<code>process</code>，表示只停止主进程，不停止任何sshd 子进程，即子进程打开的 SSH session 仍然保持连接。这个设置不太常见，但对 sshd 很重要，否则你停止服务的时候，会连自己打开的 SSH session 一起杀掉。</p><p><code>KillMode</code>字段可以设置的值如下。</p><blockquote><ul><li>control-group（默认值）：当前控制组里面的所有子进程，都会被杀掉</li><li>process：只杀主进程</li><li>mixed：主进程将收到 SIGTERM 信号，子进程收到 SIGKILL 信号</li><li>none：没有进程会被杀掉，只是执行服务的 stop 命令。</li></ul></blockquote><p>接下来是<code>Restart</code>字段。</p><blockquote><p><code>Restart</code>字段：定义了 sshd 退出后，Systemd 的重启方式。</p></blockquote><p>上面的例子中，<code>Restart</code>设为<code>on-failure</code>，表示任何意外的失败，就将重启sshd。如果 sshd 正常停止（比如执行<code>systemctl stop</code>命令），它就不会重启。</p><p><code>Restart</code>字段可以设置的值如下。</p><blockquote><ul><li>no（默认值）：退出后不会重启</li></ul></blockquote><ul><li>on-success：只有正常退出时（退出状态码为0），才会重启</li><li>on-failure：非正常退出时（退出状态码非0），包括被信号终止和超时，才会重启</li><li>on-abnormal：只有被信号终止和超时，才会重启</li><li>on-abort：只有在收到没有捕捉到的信号终止时，才会重启</li><li>on-watchdog：超时退出，才会重启</li><li>always：不管是什么退出原因，总是重启</li></ul><p>对于守护进程，推荐设为<code>on-failure</code>。对于那些允许发生错误退出的服务，可以设为<code>on-abnormal</code>。</p><p>最后是<code>RestartSec</code>字段。</p><blockquote><p><code>RestartSec</code>字段：表示 Systemd 重启服务之前，需要等待的秒数。上面的例子设为等待42秒。</p></blockquote><h2 id="七、-Install-区块"><a href="#七、-Install-区块" class="headerlink" title="七、[Install] 区块"></a>七、[Install] 区块</h2><p><code>Install</code>区块，定义如何安装这个配置文件，即怎样做到开机启动。</p><blockquote><p><code>WantedBy</code>字段：表示该服务所在的 Target。</p></blockquote><p><code>Target</code>的含义是服务组，表示一组服务。<code>WantedBy=multi-user.target</code>指的是，sshd 所在的 Target 是<code>multi-user.target</code>。</p><p>这个设置非常重要，因为执行<code>systemctl enable sshd.service</code>命令时，<code>sshd.service</code>的一个符号链接，就会放在<code>/etc/systemd/system</code>目录下面的<code>multi-user.target.wants</code>子目录之中。</p><p>Systemd 有默认的启动 Target。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl get-default<br>multi-user.target<br></code></pre></td></tr></table></figure><p>上面的结果表示，默认的启动 Target 是<code>multi-user.target</code>。在这个组里的所有服务，都将开机启动。这就是为什么<code>systemctl enable</code>命令能设置开机启动的原因。</p><p>使用 Target 的时候，<code>systemctl list-dependencies</code>命令和<code>systemctl isolate</code>命令也很有用。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 查看 multi-user.target 包含的所有服务</span><br>$ systemctl list-dependencies multi-user.target<br><br><span class="hljs-comment"># 切换到另一个 target</span><br><span class="hljs-comment"># shutdown.target 就是关机状态</span><br>$ <span class="hljs-built_in">sudo</span> systemctl isolate shutdown.target<br></code></pre></td></tr></table></figure><p>一般来说，常用的 Target 有两个：一个是<code>multi-user.target</code>，表示多用户命令行状态；另一个是<code>graphical.target</code>，表示图形用户状态，它依赖于<code>multi-user.target</code>。</p><h2 id="八、Target-的配置文件"><a href="#八、Target-的配置文件" class="headerlink" title="八、Target 的配置文件"></a>八、Target 的配置文件</h2><p>Target 也有自己的配置文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs bash">$ systemctl <span class="hljs-built_in">cat</span> multi-user.target<br><br>[Unit]<br>Description=Multi-User System<br>Documentation=man:systemd.special(7)<br>Requires=basic.target<br>Conflicts=rescue.service rescue.target<br>After=basic.target rescue.service rescue.target<br>AllowIsolate=<span class="hljs-built_in">yes</span><br></code></pre></td></tr></table></figure><p>注意，Target 配置文件里面没有启动命令。</p><p>上面输出结果中，主要字段含义如下。</p><blockquote><p><code>Requires</code>字段：要求<code>basic.target</code>一起运行。</p><p><code>Conflicts</code>字段：冲突字段。如果<code>rescue.service</code>或<code>rescue.target</code>正在运行，<code>multi-user.target</code>就不能运行，反之亦然。</p><p><code>After</code>：表示<code>multi-user.target</code>在<code>basic.target</code> 、 <code>rescue.service</code>、 <code>rescue.target</code>之后启动，如果它们有启动的话。</p><p><code>AllowIsolate</code>：允许使用<code>systemctl isolate</code>命令切换到<code>multi-user.target</code>。</p></blockquote><h2 id="九、修改配置文件后重启"><a href="#九、修改配置文件后重启" class="headerlink" title="九、修改配置文件后重启"></a>九、修改配置文件后重启</h2><p>修改配置文件以后，需要重新加载配置文件，然后重新启动相关服务。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 重新加载配置文件</span><br>$ <span class="hljs-built_in">sudo</span> systemctl daemon-reload<br><br><span class="hljs-comment"># 重启相关服务</span><br>$ <span class="hljs-built_in">sudo</span> systemctl restart foobar<br></code></pre></td></tr></table></figure><p>（完）</p>]]>
    </content>
    <id>https://sunshinechen.cn/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%882%EF%BC%89/</id>
    <link href="https://sunshinechen.cn/2024/08/21/Systemd%E8%AF%A6%E8%A7%A3%EF%BC%882%EF%BC%89/"/>
    <published>2024-08-21T09:07:35.000Z</published>
    <summary>
      <![CDATA[<h1 id="Systemd-入门教程：实战篇"><a href="#Systemd-入门教程：实战篇" class="headerlink" title="Systemd 入门教程：实战篇"></a>Systemd 入门教程：实战篇</h1><p>上一篇文章，我介绍了 Sys]]>
    </summary>
    <title>Systemd 入门教程：实战篇</title>
    <updated>2026-10-28T09:18:36.274Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<p>在 Go 中借助标准库 <code>runtime.debug</code> 这个包的<code>ReadBuildInfo</code>函数去获取编译程序的各种信息，这个信息结构体如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> BuildInfo <span class="hljs-keyword">struct</span> &#123;<br><span class="hljs-comment">// GoVersion is the version of the Go toolchain that built the binary</span><br><span class="hljs-comment">// (for example, &quot;go1.19.2&quot;).</span><br>GoVersion <span class="hljs-type">string</span><br><br><span class="hljs-comment">// Path is the package path of the main package for the binary</span><br><span class="hljs-comment">// (for example, &quot;golang.org/x/tools/cmd/stringer&quot;).</span><br>Path <span class="hljs-type">string</span><br><br><span class="hljs-comment">// Main describes the module that contains the main package for the binary.</span><br>Main Module<br><br><span class="hljs-comment">// Deps describes all the dependency modules, both direct and indirect,</span><br><span class="hljs-comment">// that contributed packages to the build of this binary.</span><br>Deps []*Module<br><br><span class="hljs-comment">// Settings describes the build settings used to build the binary.</span><br>Settings []BuildSetting<br>&#125;<br></code></pre></td></tr></table></figure><p>上面的结构体就可以获取到 go 的版本信息、路径等信息，其中<code>Settings</code>字段就包含了 git 相关的编译时间、提交 hash 等数据，<code>BuildSetting</code> 类型结构体如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> BuildSetting <span class="hljs-keyword">struct</span> &#123;<br><span class="hljs-comment">// Key and Value describe the build setting.</span><br><span class="hljs-comment">// Key must not contain an equals sign, space, tab, or newline.</span><br><span class="hljs-comment">// Value must not contain newlines (&#x27;\n&#x27;).</span><br>Key, Value <span class="hljs-type">string</span><br>&#125;<br></code></pre></td></tr></table></figure><p>其中 Key 的的取值如下：</p><ul><li>-buildmode: the buildmode flag used (typically “exe”)</li><li>-compiler: the compiler toolchain flag used (typically “gc”)</li><li>CGO_ENABLED: the effective CGO_ENABLED environment variable</li><li>CGO_CFLAGS: the effective CGO_CFLAGS environment variable</li><li>CGO_CPPFLAGS: the effective CGO_CPPFLAGS environment variable</li><li>CGO_CXXFLAGS: the effective CGO_CXXFLAGS environment variable</li><li>CGO_LDFLAGS: the effective CGO_LDFLAGS environment variable</li><li>GOARCH: the architecture target</li><li>GOAMD64&#x2F;GOARM&#x2F;GO386&#x2F;etc: the architecture feature level for GOARCH</li><li>GOOS: the operating system target</li><li>vcs: the version control system for the source tree where the build ran</li><li>vcs.revision: the revision identifier for the current commit or checkout</li><li>vcs.time: the modification time associated with vcs.revision, in RFC3339 format</li><li>vcs.modified: true or false indicating whether the source tree had local modifications</li></ul><p>从上述描述信息可以得知，<code>BuildSetting</code> 这个结构体以<code>Key/Value</code>的方式去存储各种编译信息，包含了 <code>CGO</code>,<code>GOOS</code>、<code>GOARCH</code>、<code>VCS</code>等各类信息，下面写一个程序来获取：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// buildInfo 输出编译信息中的修订版本和构建时间。</span><br><span class="hljs-comment">// 该函数尝试从debug.BuildInfo中提取VCS修订版本和构建时间，并打印出来。</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">buildInfo</span><span class="hljs-params">()</span></span> &#123;<br>    <span class="hljs-comment">// 尝试读取编译信息，如果成功则继续处理。</span><br><span class="hljs-keyword">if</span> buildInfo, ok := debug.ReadBuildInfo(); ok &#123;<br>        <span class="hljs-comment">// 在编译信息的设置中查找VCS修订版本。</span><br>revision, ok := lo.Find(buildInfo.Settings, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(setting debug.BuildSetting)</span></span> <span class="hljs-type">bool</span> &#123;<br><span class="hljs-keyword">return</span> setting.Key == <span class="hljs-string">&quot;vcs.revision&quot;</span><br>&#125;)<br><span class="hljs-keyword">if</span> ok &#123;<br>            <span class="hljs-comment">// 如果找到了修订版本，打印出来。</span><br>fmt.Println(revision.Value)<br>&#125;<br><br>        <span class="hljs-comment">// 在编译信息的设置中查找VCS构建时间。</span><br>buildTime, ok := lo.Find(buildInfo.Settings, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(setting debug.BuildSetting)</span></span> <span class="hljs-type">bool</span> &#123;<br><span class="hljs-keyword">return</span> setting.Key == <span class="hljs-string">&quot;vcs.time&quot;</span><br>&#125;)<br><span class="hljs-keyword">if</span> ok &#123;<br>            <span class="hljs-comment">// 如果找到了构建时间，解析并打印出来。</span><br>            <span class="hljs-comment">// 时间格式化为&quot;2006-01-02 15:04:05 -0700 MST&quot;。</span><br>parse, _ := time.Parse(time.RFC3339, buildTime.Value)<br>fmt.Println(parse.Format(time.DateTime))<br>&#125;<br>&#125;<br>&#125;<br><br></code></pre></td></tr></table></figure><p>该 Go 函数 buildInfo()旨在从程序的编译元数据中提取并展示两个关键信息：版本控制系统的最新修订编号（Revision）以及构建发生的具体时间。下面是该函数执行步骤的详细分解：</p><ol><li><p>读取构建信息:<br>首先调用 debug.ReadBuildInfo()尝试获取当前程序的构建元数据。此函数返回一个<code>debug.BuildInfo</code>结构体，其中包含了编译时的各种信息，如依赖关系、编译选项等。</p></li><li><p>查找 VCS 修订号:<br>使用 lo.Find()函数（假设 lo 包已导入，这是一个辅助库提供列表处理功能），遍历 buildInfo.Settings 切片来查找键为”vcs.revision”的条目。这是为了提取版本控制系统中的最后一次提交的修订标识。<br>如果找到匹配的设置项，则通过 revision.Value 打印出该修订号。</p></li><li><p>查找并格式化构建时间:<br>同样利用 lo.Find()方法，在设置中寻找键为”vcs.time”的条目，该条目通常包含最后一次 VCS 提交的时间戳。<br>发现时间戳后，尝试使用 time.Parse()函数按照 RFC3339 格式解析该时间字符串到 time.Time 类型。此步骤假设时间数据是符合 RFC3339 标准的字符串。</p></li></ol>]]>
    </content>
    <id>https://sunshinechen.cn/2024/06/19/Go%E5%AE%9E%E7%8E%B0%E8%8E%B7%E5%8F%96%E7%BC%96%E8%AF%91%E7%A8%8B%E5%BA%8F%E7%9A%84git%E7%BC%96%E8%AF%91%E6%97%B6%E9%97%B4%E3%80%81%E6%8F%90%E4%BA%A4%E6%97%B6%E9%97%B4%E7%AD%89%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/</id>
    <link href="https://sunshinechen.cn/2024/06/19/Go%E5%AE%9E%E7%8E%B0%E8%8E%B7%E5%8F%96%E7%BC%96%E8%AF%91%E7%A8%8B%E5%BA%8F%E7%9A%84git%E7%BC%96%E8%AF%91%E6%97%B6%E9%97%B4%E3%80%81%E6%8F%90%E4%BA%A4%E6%97%B6%E9%97%B4%E7%AD%89%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/"/>
    <published>2024-06-19T07:12:04.000Z</published>
    <summary>
      <![CDATA[<p>在 Go 中借助标准库 <code>runtime.debug</code> 这个包的<code>ReadBuildInfo</code>函数去获取编译程序的各种信息，这个信息结构体如下：</p>
<figure class="highlight go"><table><t]]>
    </summary>
    <title>Go实现获取编译程序的git编译时间、提交时间等环境变量</title>
    <updated>2026-10-28T09:18:36.406Z</updated>
  </entry>
  <entry>
    <author>
      <name>jianhao</name>
    </author>
    <category term="程序开发" scheme="https://sunshinechen.cn/categories/%E7%A8%8B%E5%BA%8F%E5%BC%80%E5%8F%91/"/>
    <category term="Go" scheme="https://sunshinechen.cn/tags/Go/"/>
    <content>
      <![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p><code>gcache</code>是一个使用Go语言实现的内存缓存库，在github上有2.5k个star，支持LFU、LRU、ARC等多种缓存驱逐策略，可有利于对相对应思想的实现。</p><p>但该库最近的一次更新已经是2021年的，所以该库不推荐用于实际生产，仅作为源码学习使用。</p><p>根据KHWL图标来列举出一些知识点以及我想要学习的地方，如下图：</p><p><img src="https://images-1306554305.cos.ap-guangzhou.myqcloud.com/2024-03-13_14-42.png"></p><h2 id="快速使用"><a href="#快速使用" class="headerlink" title="快速使用"></a>快速使用</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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><code class="hljs go"><span class="hljs-keyword">package</span> main<br><br><span class="hljs-keyword">import</span> (<br>  <span class="hljs-string">&quot;github.com/bluele/gcache&quot;</span><br>  <span class="hljs-string">&quot;fmt&quot;</span><br>)<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br>  gc := gcache.New(<span class="hljs-number">20</span>).<br>    LRU().<br>    Build()<br>  gc.Set(<span class="hljs-string">&quot;key&quot;</span>, <span class="hljs-string">&quot;ok&quot;</span>)<br>  value, err := gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>    <span class="hljs-built_in">panic</span>(err)<br>  &#125;<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br>&#125;<br></code></pre></td></tr></table></figure><p>运行结果：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">Get: ok<br></code></pre></td></tr></table></figure><p>我们调用了<code>gcache</code>库的<code>New</code>方法，传入一个int值，然后采用链式调用的方法，调用<code>LRU</code>方法，最后调用<code>Build</code>方法来初始化一个对象。然后使用该对象设置键值对，最后获取并打印。</p><p>在上面代码中可以看出，初始化对象时该库使用了建造者模式，只有一个必传参数也就是<code>New</code>方法的<code>size</code>参数，该参数代表内存中可存储的最大键值对数量，其他参数都可通过链式方法进行调用。</p><blockquote><p><strong>说明</strong><br>在Go中，除了这种建造者模式，还可以通过闭包来实现<a href="/2023/04/18/Go%E7%9A%84%E5%87%A0%E7%A7%8D%E5%87%BD%E6%95%B0%E4%BC%A0%E5%8F%82%E6%A8%A1%E5%BC%8F/">函数式传参</a>。</p></blockquote><p>获取对象之后，就像其他内存库一样，调用<code>Set</code>和<code>Get</code>方法即可设置和获取键值对。</p><h3 id="缓存过期时间"><a href="#缓存过期时间" class="headerlink" title="缓存过期时间"></a>缓存过期时间</h3><p>在使用该库去设置键值对的时候，默认是不会过期的，除非你显示的调用<code>SetWithExpire</code>方法并传入<code>time.Duration</code>参数，这样才会有指定的时间将缓存删除掉。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">package</span> main<br><br><span class="hljs-keyword">import</span> (<br>  <span class="hljs-string">&quot;github.com/bluele/gcache&quot;</span><br>  <span class="hljs-string">&quot;fmt&quot;</span><br>  <span class="hljs-string">&quot;time&quot;</span><br>)<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br>  gc := gcache.New(<span class="hljs-number">20</span>).<br>    LRU().<br>    Build()<br>  gc.SetWithExpire(<span class="hljs-string">&quot;key&quot;</span>, <span class="hljs-string">&quot;ok&quot;</span>, time.Second*<span class="hljs-number">10</span>)<br>  value, _ := gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br><br>  <span class="hljs-comment">// Wait for value to expire</span><br>  time.Sleep(time.Second*<span class="hljs-number">10</span>)<br><br>  value, err := gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>    <span class="hljs-built_in">panic</span>(err)<br>  &#125;<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br>&#125;<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs go">Get: ok<br><span class="hljs-comment">// 10 seconds later, new attempt:</span><br><span class="hljs-built_in">panic</span>: ErrKeyNotFound<br></code></pre></td></tr></table></figure><p>可以看出在设置<code>key</code>同时指定了一个过期时间，在等待了指定时间后再去获取，该key已经从内存中删除了。</p><h3 id="当指定数据不存在时自动获取"><a href="#当指定数据不存在时自动获取" class="headerlink" title="当指定数据不存在时自动获取"></a>当指定数据不存在时自动获取</h3><p>当获取一个不存在的key时，支持自动的去设置该key的value。为了实现这个效果，<code>gcache</code>暴露了一个<code>LoaderFunc</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><code class="hljs go"><span class="hljs-keyword">type</span> (  <br>    LoaderFunc       <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(<span class="hljs-keyword">interface</span>&#123;&#125;)</span></span> (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>)  <br><span class="hljs-comment">// 其他类型</span><br>)<br></code></pre></td></tr></table></figure><p>这是一个函数类型，参数是对应不存在值的<code>key</code>，返回一个数据和<code>error</code>，下面是使用方式：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">package</span> main<br><br><span class="hljs-keyword">import</span> (<br>  <span class="hljs-string">&quot;github.com/bluele/gcache&quot;</span><br>  <span class="hljs-string">&quot;fmt&quot;</span><br>)<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br>  gc := gcache.New(<span class="hljs-number">20</span>).<br>    LRU().<br>    LoaderFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(key <span class="hljs-keyword">interface</span>&#123;&#125;)</span></span> (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;<br>      <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;ok&quot;</span>, <span class="hljs-literal">nil</span><br>    &#125;).<br>    Build()<br>  value, err := gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>    <span class="hljs-built_in">panic</span>(err)<br>  &#125;<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br>&#125;<br></code></pre></td></tr></table></figure><p>从上面代码可以看出，在创建缓存对象时传入了一个<code>LoaderFunc</code>匿名函数，始终返回<code>ok</code>字符串和空错误，所以在获取<code>key</code>这个对象时，会直接返回<code>ok</code>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs go">Get: ok<br></code></pre></td></tr></table></figure><p>在此基础上，同样支持对指定<code>key</code>和<code>value</code>的<code>expire</code>过期时间，使用方式就是将上面的<code>LoaderFunc</code>方法替换成<code>LoaderExpireFunc</code>方法，该方法会多添加一个返回值，就是该<code>key</code>的过期时间。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">package</span> main<br><br><span class="hljs-keyword">import</span> (<br>  <span class="hljs-string">&quot;fmt&quot;</span><br>  <span class="hljs-string">&quot;time&quot;</span><br><br>  <span class="hljs-string">&quot;github.com/bluele/gcache&quot;</span><br>)<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br>  <span class="hljs-keyword">var</span> evictCounter, loaderCounter, purgeCounter <span class="hljs-type">int</span><br>  gc := gcache.New(<span class="hljs-number">20</span>).<br>    LRU().<br>    LoaderExpireFunc(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(key <span class="hljs-keyword">interface</span>&#123;&#125;)</span></span> (<span class="hljs-keyword">interface</span>&#123;&#125;, *time.Duration, <span class="hljs-type">error</span>) &#123;<br>      expire := <span class="hljs-number">1</span> * time.Second<br>      <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;ok&quot;</span>, &amp;expire, <span class="hljs-literal">nil</span><br>    &#125;).<br>    Build()<br>  value, err := gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>    <span class="hljs-built_in">panic</span>(err)<br>  &#125;<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br>  time.Sleep(<span class="hljs-number">1</span> * time.Second)<br>  value, err = gc.Get(<span class="hljs-string">&quot;key&quot;</span>)<br>  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;<br>    <span class="hljs-built_in">panic</span>(err)<br>  &#125;<br>  fmt.Println(<span class="hljs-string">&quot;Get:&quot;</span>, value)<br>&#125;<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs go">Get: ok<br>Get: ok<br></code></pre></td></tr></table></figure><p>内存缓存的一些通用操作它都有，比如删除、所有<code>key</code>、获取缓存中的<code>key</code>的数量、删除所有<code>key</code>、判断一个<code>key</code>是否在缓存中等等。</p><h2 id="源码实现"><a href="#源码实现" class="headerlink" title="源码实现"></a>源码实现</h2><h3 id="CacheBuilder"><a href="#CacheBuilder" class="headerlink" title="CacheBuilder"></a>CacheBuilder</h3><p>首先从库的包级函数<code>New</code>入手，该函数返回<code>CacheBuilder</code>结构体，用于构建<code>Cache</code>对象，对象字段如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> CacheBuilder <span class="hljs-keyword">struct</span> &#123;  <br>    clock            Clock  <br>    tp               <span class="hljs-type">string</span>  <br>    size             <span class="hljs-type">int</span>  <br>    loaderExpireFunc LoaderExpireFunc  <br>    evictedFunc      EvictedFunc  <br>    purgeVisitorFunc PurgeVisitorFunc  <br>    addedFunc        AddedFunc  <br>    expiration       *time.Duration  <br>    deserializeFunc  DeserializeFunc  <br>    serializeFunc    SerializeFunc  <br>&#125;  <br>  <br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">New</span><span class="hljs-params">(size <span class="hljs-type">int</span>)</span></span> *CacheBuilder &#123;  <br>    <span class="hljs-keyword">return</span> &amp;CacheBuilder&#123;  <br>       clock: NewRealClock(),  <br>       tp:    TYPE_SIMPLE,  <br>       size:  size,  <br>    &#125;  <br>&#125;<br></code></pre></td></tr></table></figure><p>对应链式调用函数就将该对象的字段设置成入参，然后返回该对象指针，例如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> Simple() *CacheBuilder &#123;  <br>    <span class="hljs-keyword">return</span> cb.EvictType(TYPE_SIMPLE)  <br>&#125;  <br>  <br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> LRU() *CacheBuilder &#123;  <br>    <span class="hljs-keyword">return</span> cb.EvictType(TYPE_LRU)  <br>&#125;  <br>  <br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> LFU() *CacheBuilder &#123;  <br>    <span class="hljs-keyword">return</span> cb.EvictType(TYPE_LFU)  <br>&#125;  <br>  <br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> ARC() *CacheBuilder &#123;  <br>    <span class="hljs-keyword">return</span> cb.EvictType(TYPE_ARC)  <br>&#125;<br></code></pre></td></tr></table></figure><p>上述代码设置该缓存对象的过期对象驱逐策略，将<code>EvictType</code>设置成对应的常量，然后调用<code>Build</code>方法开始构建：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> Build() Cache &#123;  <br><span class="hljs-comment">// 参数校验</span><br>    <span class="hljs-keyword">if</span> cb.size &lt;= <span class="hljs-number">0</span> &amp;&amp; cb.tp != TYPE_SIMPLE &#123;  <br>       <span class="hljs-built_in">panic</span>(<span class="hljs-string">&quot;gcache: Cache size &lt;= 0&quot;</span>)  <br>    &#125;  <br>  <br>    <span class="hljs-keyword">return</span> cb.build()  <br>&#125;  <br><br><span class="hljs-comment">// Cache是一个接口，包含了对缓存的通用操作</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(cb *CacheBuilder)</span></span> build() Cache &#123;<br><span class="hljs-comment">// 根据驱逐策略选择的不同去实例化不同对象</span><br>    <span class="hljs-keyword">switch</span> cb.tp &#123;  <br>    <span class="hljs-keyword">case</span> TYPE_SIMPLE:  <br>       <span class="hljs-keyword">return</span> newSimpleCache(cb)  <br>    <span class="hljs-keyword">case</span> TYPE_LRU:  <br>       <span class="hljs-keyword">return</span> newLRUCache(cb)  <br>    <span class="hljs-keyword">case</span> TYPE_LFU:  <br>       <span class="hljs-keyword">return</span> newLFUCache(cb)  <br>    <span class="hljs-keyword">case</span> TYPE_ARC:  <br>       <span class="hljs-keyword">return</span> newARC(cb)  <br>    <span class="hljs-keyword">default</span>:  <br>       <span class="hljs-built_in">panic</span>(<span class="hljs-string">&quot;gcache: Unknown type &quot;</span> + cb.tp)  <br>    &#125;  <br>&#125;<br></code></pre></td></tr></table></figure><p>如果不选择驱逐策略，那么就是默认的<code>TYPE_SIMPLE</code>类型，则不驱逐缓存。</p><h3 id="Cache"><a href="#Cache" class="headerlink" title="Cache"></a>Cache</h3><p>在<code>gcache</code>中定义了一个接口<code>Cache</code>，一个驱逐策略就对应一个结构体，实现该接口，以下是接口的定义：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Cache <span class="hljs-keyword">interface</span> &#123;  <br>    <span class="hljs-comment">// Set inserts or updates the specified key-value pair.    </span><br>    Set(key, value <span class="hljs-keyword">interface</span>&#123;&#125;) <span class="hljs-type">error</span>  <br>    <span class="hljs-comment">// SetWithExpire inserts or updates the specified key-value pair with an expiration time.    </span><br>    SetWithExpire(key, value <span class="hljs-keyword">interface</span>&#123;&#125;, expiration time.Duration) <span class="hljs-type">error</span>  <br>    <span class="hljs-comment">// Get returns the value for the specified key if it is present in the cache.    // If the key is not present in the cache and the cache has LoaderFunc,  </span><br>    <span class="hljs-comment">// invoke the `LoaderFunc` function and inserts the key-value pair in the cache.    // If the key is not present in the cache and the cache does not have a LoaderFunc,  </span><br>    <span class="hljs-comment">// return KeyNotFoundError.  </span><br>    Get(key <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>)  <br>    <span class="hljs-comment">// GetIFPresent returns the value for the specified key if it is present in the cache.    </span><br>    <span class="hljs-comment">// Return KeyNotFoundError if the key is not present.    </span><br>    GetIFPresent(key <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>)  <br>    <br>    <span class="hljs-comment">// GetAll returns a map containing all key-value pairs in the cache.  </span><br>    GetALL(checkExpired <span class="hljs-type">bool</span>) <span class="hljs-keyword">map</span>[<span class="hljs-keyword">interface</span>&#123;&#125;]<span class="hljs-keyword">interface</span>&#123;&#125;  <br>    get(key <span class="hljs-keyword">interface</span>&#123;&#125;, onLoad <span class="hljs-type">bool</span>) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>)  <br>    <span class="hljs-comment">// Remove removes the specified key from the cache if the key is present.    </span><br>    <span class="hljs-comment">// Returns true if the key was present and the key has been deleted.    </span><br>    Remove(key <span class="hljs-keyword">interface</span>&#123;&#125;) <span class="hljs-type">bool</span>  <br>    <span class="hljs-comment">// Purge removes all key-value pairs from the cache.    </span><br>    Purge()  <br>    <span class="hljs-comment">// Keys returns a slice containing all keys in the cache.    </span><br>    Keys(checkExpired <span class="hljs-type">bool</span>) []<span class="hljs-keyword">interface</span>&#123;&#125;  <br>    <span class="hljs-comment">// Len returns the number of items in the cache.    </span><br>    Len(checkExpired <span class="hljs-type">bool</span>) <span class="hljs-type">int</span>  <br>    <span class="hljs-comment">// Has returns true if the key exists in the cache.    </span><br>    Has(key <span class="hljs-keyword">interface</span>&#123;&#125;) <span class="hljs-type">bool</span>  <br>&#125;<br></code></pre></td></tr></table></figure><p>定义了内存库中的一些常规操作。</p><h4 id="SimpleCache"><a href="#SimpleCache" class="headerlink" title="SimpleCache"></a>SimpleCache</h4><p>在初始化缓存对象时如果指定具体的内存驱逐策略，则使用<code>SimpleCache</code>结构来作为默认的缓存对象，这个对象结构比较简单，处理在建造者模式中获取到的其他数据之外，还有一个<code>map</code>类型的变量<code>items</code>来存放对应的键值对，<code>value</code>是一个包装后的<code>simpleItem</code>结构体。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">type</span> baseCache struct &#123;  <br>    clock            Clock  <br>    size             int  <br>    loaderExpireFunc LoaderExpireFunc  <br>    evictedFunc      EvictedFunc  <br>    purgeVisitorFunc PurgeVisitorFunc  <br>    addedFunc        AddedFunc  <br>    deserializeFunc  DeserializeFunc  <br>    serializeFunc    SerializeFunc  <br>    expiration       *time.Duration  <br>    mu               sync.RWMutex  <br>    loadGroup        Group  <br>    *stats  <br>&#125;<br><br><span class="hljs-built_in">type</span> simpleItem struct &#123;  <br>    clock      Clock  <br>    value      interface&#123;&#125;<br>    // 这个value的过期时间  <br>    expiration *time.Time  <br>&#125;<br><br>// SimpleCache has no clear priority <span class="hljs-keyword">for</span> evict cache. It depends on key-value map order.<br><span class="hljs-built_in">type</span> SimpleCache struct &#123;  <br>    baseCache  <br>    items map[interface&#123;&#125;]*simpleItem  <br>&#125;<br></code></pre></td></tr></table></figure><p><code>SimpleCache</code>对象实现了<code>Cache</code>接口，现在来看这个库设置key时是如何处理过期时间的，在调用<code>Set()</code>和<code>SetWithExpirce()</code>方法时，最终都会调用<code>simpleCache.set()</code>方法，只不过这两者的区别是<code>SetWithExpirce</code>方法会计算过期时间，并将时间设置到<code>simpleItem</code>对象的<code>expiration</code>字段。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// Set a new key-value pair</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> Set(key, value <span class="hljs-keyword">interface</span>&#123;&#125;) <span class="hljs-type">error</span> &#123;  <br>    c.mu.Lock()  <br>    <span class="hljs-keyword">defer</span> c.mu.Unlock()  <br>    _, err := c.set(key, value)  <br>    <span class="hljs-keyword">return</span> err  <br>&#125;<br><br><span class="hljs-comment">// Set a new key-value pair with an expiration time</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> SetWithExpire(key, value <span class="hljs-keyword">interface</span>&#123;&#125;, expiration time.Duration) <span class="hljs-type">error</span> &#123;  <br>    c.mu.Lock()  <br>    <span class="hljs-keyword">defer</span> c.mu.Unlock()  <br>    item, err := c.set(key, value)  <br>    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;  <br>       <span class="hljs-keyword">return</span> err  <br>    &#125;  <br><span class="hljs-comment">// 计算这个key的过期时间，并将该时间设置到simpleItem的expiration字段</span><br>    t := c.clock.Now().Add(expiration)  <br>    item.(*simpleItem).expiration = &amp;t  <br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>  <br>&#125;<br><br><span class="hljs-comment">// simpleCache的核心逻辑函数</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> set(key, value <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    <span class="hljs-keyword">var</span> err <span class="hljs-type">error</span><br>    <span class="hljs-comment">// 查看这个缓存是否有设置自己的序列化函数，有的话就序列化，反之跳过  </span><br>    <span class="hljs-keyword">if</span> c.serializeFunc != <span class="hljs-literal">nil</span> &#123;  <br>       value, err = c.serializeFunc(key, value)  <br>       <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;  <br>          <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err  <br>       &#125;  <br>    &#125;  <br>  <br>    <span class="hljs-comment">// Check for existing item  </span><br>    item, ok := c.items[key]<br>    <span class="hljs-comment">// 查看该key是否已经在缓存中，如果有则更新  </span><br>    <span class="hljs-keyword">if</span> ok &#123;  <br>       item.value = value  <br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>       <span class="hljs-comment">// 没有的话则要判断当前缓存是否已经满了，满了的话则要驱逐</span><br>       <span class="hljs-keyword">if</span> (<span class="hljs-built_in">len</span>(c.items) &gt;= c.size) &amp;&amp; c.size &gt; <span class="hljs-number">0</span> &#123;  <br>      <span class="hljs-comment">// 驱逐一个缓存</span><br>          c.evict(<span class="hljs-number">1</span>)  <br>       &#125;  <br>       item = &amp;simpleItem&#123;  <br>          clock: c.clock,  <br>          value: value,  <br>       &#125;  <br>       c.items[key] = item  <br>    &#125;  <br><br>    <span class="hljs-keyword">if</span> c.expiration != <span class="hljs-literal">nil</span> &#123;  <br>       t := c.clock.Now().Add(*c.expiration)  <br>       item.expiration = &amp;t  <br>    &#125;  <br>  <br>    <span class="hljs-keyword">if</span> c.addedFunc != <span class="hljs-literal">nil</span> &#123;  <br>       c.addedFunc(key, value)  <br>    &#125;  <br>  <br>    <span class="hljs-keyword">return</span> item, <span class="hljs-literal">nil</span>  <br>&#125;<br></code></pre></td></tr></table></figure><p>上面的逻辑也很简单，先判断是否设置了序列化函数，如果有则先将值进行序列化。然后判断该<code>key</code>数据是否在缓存中存在，如果存在则替换，不存在的话判断当前缓存的数量是否已经满了，满了则驱逐一个，否则直接添加。</p><p>然后判断是否有自定义添加<code>key</code>的回调函数，有则调用。</p><p>那在<code>SimpleCache</code>是如何判断该<code>key</code>过期的呢，是在<code>simpleItem</code>结构体里定义了一个<code>IsExpired</code>方法，传入一个<code>time.Time</code>类型。</p><p><code>map</code>中对应存储的<code>value</code>是一个<code>simpleItem</code>结构，并且存储了<code>expiration</code>字段作为该值的过期时间。调用<code>time.Time</code>的<code>Before</code>方法，来判断过期时间是否在传入时间之前，返回该布尔值，代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// 外部应用调用的方法</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> Get(key <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    v, err := c.get(key, <span class="hljs-literal">false</span>)  <br>    <span class="hljs-keyword">if</span> err == KeyNotFoundError &#123;  <br>       <span class="hljs-keyword">return</span> c.getWithLoader(key, <span class="hljs-literal">true</span>)  <br>    &#125;  <br>    <span class="hljs-keyword">return</span> v, err  <br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> get(key <span class="hljs-keyword">interface</span>&#123;&#125;, onLoad <span class="hljs-type">bool</span>) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;<br><span class="hljs-comment">// 获取key</span><br>    v, err := c.getValue(key, onLoad)  <br>    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123;  <br>       <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err  <br>    &#125;<br>    <span class="hljs-comment">// 解码  </span><br>    <span class="hljs-keyword">if</span> c.deserializeFunc != <span class="hljs-literal">nil</span> &#123;  <br>       <span class="hljs-keyword">return</span> c.deserializeFunc(key, v)  <br>    &#125;  <br>    <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>  <br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> getValue(key <span class="hljs-keyword">interface</span>&#123;&#125;, onLoad <span class="hljs-type">bool</span>) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    c.mu.Lock()  <br>    item, ok := c.items[key]  <br>    <span class="hljs-keyword">if</span> ok &#123;<br>    <span class="hljs-comment">// 判断  </span><br>       <span class="hljs-keyword">if</span> !item.IsExpired(<span class="hljs-literal">nil</span>) &#123;  <br>          v := item.value  <br>          c.mu.Unlock()  <br>          <span class="hljs-keyword">if</span> !onLoad &#123;  <br>             c.stats.IncrHitCount()  <br>          &#125;  <br>          <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>  <br>       &#125;  <br>       c.remove(key)  <br>    &#125;  <br>    c.mu.Unlock()  <br>    <span class="hljs-keyword">if</span> !onLoad &#123;  <br>       c.stats.IncrMissCount()  <br>    &#125;  <br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, KeyNotFoundError  <br>&#125;<br><br><span class="hljs-comment">// IsExpired returns boolean value whether this item is expired or not.</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(si *simpleItem)</span></span> IsExpired(now *time.Time) <span class="hljs-type">bool</span> &#123;  <br>    <span class="hljs-keyword">if</span> si.expiration == <span class="hljs-literal">nil</span> &#123;  <br>       <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>  <br>    &#125;  <br>    <span class="hljs-keyword">if</span> now == <span class="hljs-literal">nil</span> &#123;  <br>       t := si.clock.Now()  <br>       now = &amp;t  <br>    &#125;<br>    <span class="hljs-comment">// 判断是否过期  </span><br>    <span class="hljs-keyword">return</span> si.expiration.Before(*now)  <br>&#125;<br></code></pre></td></tr></table></figure><p>还有一个点，那就是<code>simpleCache</code>在键值对达到上限之后，是如何驱逐内存的。在上面的代码上可以看出，是调用了<code>evict</code>函数，实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *SimpleCache)</span></span> evict(count <span class="hljs-type">int</span>) &#123;  <br>    now := c.clock.Now()  <br>    current := <span class="hljs-number">0</span>  <br>    <span class="hljs-keyword">for</span> key, item := <span class="hljs-keyword">range</span> c.items &#123;  <br>       <span class="hljs-keyword">if</span> current &gt;= count &#123;  <br>          <span class="hljs-keyword">return</span>  <br>       &#125;  <br>       <span class="hljs-keyword">if</span> item.expiration == <span class="hljs-literal">nil</span> || now.After(*item.expiration) &#123;  <br>          <span class="hljs-keyword">defer</span> c.remove(key)  <br>          current++  <br>       &#125;  <br>    &#125;  <br>&#125;<br></code></pre></td></tr></table></figure><p>从代码中可以看出，就是遍历该<code>map</code>，然后将遍历到的第一个过期的<code>value</code>删除。</p><h4 id="LFU"><a href="#LFU" class="headerlink" title="LFU"></a>LFU</h4><p>LFU（最少使用）算法会淘汰缓存中访问次数最少的数据。<br>其核心原则是，如果数据过去被访问多次，那么将来被访问的频率也会更高。<br>在 LFU 实现中，通常需要维护一个按照访问次数排序的队列，每次访问时，次数加1，队伍重新排序，淘汰时选择访问次数最少的即可。</p><p>在<code>gcache</code>中，选择是用<code>list</code>来存放访问频率，并且类似<code>simpleCache</code>一样，将存入的数据封装成一个对象，该对象也包含了该<code>list</code>节点的指针，这样被存放的对象和存放频率的<code>list</code>对象就能相互引用。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> lfuItem <span class="hljs-keyword">struct</span> &#123;  <br>    clock       Clock  <br>    key         <span class="hljs-keyword">interface</span>&#123;&#125;  <br>    value       <span class="hljs-keyword">interface</span>&#123;&#125;<br>    <span class="hljs-comment">// 这里的list是标准库实现包  </span><br>    freqElement *list.Element  <br>    expiration  *time.Time  <br>&#125;<br><br><span class="hljs-comment">// 链表中实际存放的元素类型</span><br><span class="hljs-keyword">type</span> freqEntry <span class="hljs-keyword">struct</span> &#123;<br><span class="hljs-comment">// 代表访问频率</span><br>    freq  <span class="hljs-type">uint</span><br>    <span class="hljs-comment">// 当前访问频率的元素  </span><br>    items <span class="hljs-keyword">map</span>[*lfuItem]<span class="hljs-keyword">struct</span>&#123;&#125;  <br>&#125;<br><br><span class="hljs-comment">// 该方法会初始化LFU cache的时候调用</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> init() &#123;  <br>    c.freqList = list.New()  <br>    c.items = <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">interface</span>&#123;&#125;]*lfuItem, c.size)  <br>    c.freqList.PushFront(&amp;freqEntry&#123;  <br>       freq:  <span class="hljs-number">0</span>,  <br>       items: <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[*lfuItem]<span class="hljs-keyword">struct</span>&#123;&#125;),  <br>    &#125;)  <br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> set(key, value <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br><span class="hljs-comment">// 精简掉一些不关主流程的代码</span><br>    <span class="hljs-comment">// Check for existing item  </span><br>    item, ok := c.items[key]  <br>    <span class="hljs-keyword">if</span> ok &#123;  <br>       item.value = value  <br>    &#125; <span class="hljs-keyword">else</span> &#123;  <br>       <span class="hljs-comment">// 如果当前大小大于预先设定好的数量，则驱逐一个  </span><br>       <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(c.items) &gt;= c.size &#123;  <br>          c.evict(<span class="hljs-number">1</span>)  <br>       &#125;  <br>       item = &amp;lfuItem&#123;  <br>          clock:       c.clock,  <br>          key:         key,  <br>          value:       value,  <br>          freqElement: <span class="hljs-literal">nil</span>,  <br>       &#125;  <br>       <span class="hljs-comment">// 获取链表第一个节点</span><br>       el := c.freqList.Front()<br>       <span class="hljs-comment">// 进行类型断言，获取到正确类型对象</span><br>       fe := el.Value.(*freqEntry)<br>       <span class="hljs-comment">// 在当前访问频率的对象中将新增节点添加</span><br>       fe.items[item] = <span class="hljs-keyword">struct</span>&#123;&#125;&#123;&#125;<br><br>   <span class="hljs-comment">// 然后将代表访问频率的指针对象也赋值给value对象</span><br>       item.freqElement = el<br>       c.items[key] = item<br>    &#125;<br>  <br><span class="hljs-comment">// 精简掉一些不关主流程的代码</span><br>    <span class="hljs-keyword">return</span> item, <span class="hljs-literal">nil</span>  <br>&#125;<br></code></pre></td></tr></table></figure><p>从上面代码可以看出，在新增一个缓存对象时，将缓存对象包装成<code>lfuItem</code>类型，因为是新增，所以访问频率是1，从<code>LFUCache</code>类型对象里获取链表的第一个节点（即访问频率为0的节点），该链表节点实际存储的类型是<code>freqEntry</code>类型，使用一个<code>map</code>来存储当前访问频率的元素集合，将新增元素添加到<code>map</code>中。最后也将该链表节点的指针赋值给<code>lfuItem</code>。</p><p>为什么要将链表节点的指针赋值给<code>lfuItem</code>中呢？因为要在后面更新频率是用到。<code>gcache</code>是如何更新访问频率的呢？</p><p>访问一次缓存就会更新频率，源码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> getValue(key <span class="hljs-keyword">interface</span>&#123;&#125;, onLoad <span class="hljs-type">bool</span>) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    c.mu.Lock()  <br>    item, ok := c.items[key]  <br>    <span class="hljs-keyword">if</span> ok &#123;<br>   <span class="hljs-comment">// 缓存中存在这个key并且没有过期时才会更新频率  </span><br>       <span class="hljs-keyword">if</span> !item.IsExpired(<span class="hljs-literal">nil</span>) &#123;<br>          <span class="hljs-comment">// 更新频率的实现函数  </span><br>          c.increment(item)  <br>          v := item.value  <br>          c.mu.Unlock()  <br>          <span class="hljs-comment">// 精简其他代码  </span><br>          <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>  <br>       &#125;<br>       <span class="hljs-comment">// 缓存存在该key但过期了，那就删除该项  </span><br>       c.removeItem(item)  <br>    &#125;<br>    <span class="hljs-comment">// 精简其他代码  </span><br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, KeyNotFoundError  <br>&#125;<br><br><span class="hljs-comment">// 实际更新频率的函数</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> increment(item *lfuItem) &#123;<br><span class="hljs-comment">// 获取缓存对象所对应的链表节点</span><br>    currentFreqElement := item.freqElement<br>    <span class="hljs-comment">// 将链表节点通过类型断言转换成freqEntry  </span><br>    currentFreqEntry := currentFreqElement.Value.(*freqEntry)<br>    <span class="hljs-comment">// 访问频率加1  </span><br>    nextFreq := currentFreqEntry.freq + <span class="hljs-number">1</span><br>    <span class="hljs-comment">// 从链表节点的map中删除该缓存对象，因为它的访问频率已经加1了  </span><br>    <span class="hljs-built_in">delete</span>(currentFreqEntry.items, item)  <br>  <br>    <span class="hljs-comment">// a boolean whether reuse the empty current entry</span><br>    <span class="hljs-comment">// 判断这个链表节点是否要被删除，判断条件是当前节点的freq不等于0并且该节点的map长度为0</span><br>    removable := isRemovableFreqEntry(currentFreqEntry)  <br>  <br>    <span class="hljs-comment">// 获取当前节点的下一个节点  </span><br>    nextFreqElement := currentFreqElement.Next()  <br>    <span class="hljs-keyword">switch</span> &#123;  <br>    <span class="hljs-keyword">case</span> nextFreqElement == <span class="hljs-literal">nil</span> || nextFreqElement.Value.(*freqEntry).freq &gt; nextFreq:<br>       <span class="hljs-comment">// 如果当前节点的下一个节点是nil的</span><br>       <span class="hljs-comment">// 或者当前节点的下一个节点的频率大于预期频率</span><br>       <span class="hljs-keyword">if</span> removable &#123;<br>      <span class="hljs-comment">// 当前节点的freq不为0并且该节点的map长度为0</span><br>      <span class="hljs-comment">// 用预期频率替换掉当前节点的freq  </span><br>          currentFreqEntry.freq = nextFreq  <br>          nextFreqElement = currentFreqElement  <br>       &#125; <span class="hljs-keyword">else</span> &#123;<br>          <span class="hljs-comment">// 当前节点还有其他缓存所以不删除，在当前节点的后面新增一个节点</span><br>          nextFreqElement = c.freqList.InsertAfter(&amp;freqEntry&#123;  <br>             freq:  nextFreq,  <br>             items: <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[*lfuItem]<span class="hljs-keyword">struct</span>&#123;&#125;),  <br>          &#125;, currentFreqElement)  <br>       &#125;  <br>    <span class="hljs-keyword">case</span> nextFreqElement.Value.(*freqEntry).freq == nextFreq:<br>       <span class="hljs-comment">// 当前节点的下一个节点的freq正好是预期频率  </span><br>       <span class="hljs-keyword">if</span> removable &#123;  <br>          c.freqList.Remove(currentFreqElement)  <br>       &#125;  <br>    <span class="hljs-keyword">default</span>:  <br>       <span class="hljs-built_in">panic</span>(<span class="hljs-string">&quot;unreachable&quot;</span>)  <br>    &#125;<br>    <span class="hljs-comment">// 然后在链表节点里的map添加缓存对象</span><br>    nextFreqElement.Value.(*freqEntry).items[item] = <span class="hljs-keyword">struct</span>&#123;&#125;&#123;&#125;  <br>    item.freqElement = nextFreqElement  <br>&#125;<br><br></code></pre></td></tr></table></figure><p>从上面代码可以看出，从缓存对象中获取链表节点指针，将指针通过类型断言转换成<code>freqEntry</code>类型，然后将缓存对象从<code>freqEntry</code>类型中的<code>map</code>中删除，把频率自增1并赋值给 <code>nextFreq</code>。</p><p>再获取当前链表节点的下一个节点，判断是否为<code>nil</code> 或者 下一个节点的 <code>freq</code> 值大于 <code>nextFreq</code>，再根据当前链表节点是否要删除再选择是要插入一个新节点还是在当前节点添加。</p><p>然后在更新下缓存对象和链表节点的双向指针引用，这样就完成了更新频率。</p><p>访问频率低的总会是在链表的第一个节点，所以在开始驱逐的时候，就从链表第一个节点开始删除，链表节点存放这一个<code>map</code>，代表当前频率的缓存对象，然后遍历这个<code>map</code>，将获取到的元素删除掉。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> evict(count <span class="hljs-type">int</span>) &#123;  <br>    entry := c.freqList.Front()  <br>    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; count; &#123;  <br>       <span class="hljs-keyword">if</span> entry == <span class="hljs-literal">nil</span> &#123;  <br>          <span class="hljs-keyword">return</span>  <br>       &#125; <span class="hljs-keyword">else</span> &#123;  <br>          <span class="hljs-keyword">for</span> item := <span class="hljs-keyword">range</span> entry.Value.(*freqEntry).items &#123;  <br>             <span class="hljs-keyword">if</span> i &gt;= count &#123;  <br>                <span class="hljs-keyword">return</span>  <br>             &#125;  <br>             c.removeItem(item)  <br>             i++  <br>          &#125;  <br>          entry = entry.Next()  <br>       &#125;  <br>    &#125;  <br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LFUCache)</span></span> removeItem(item *lfuItem) &#123;  <br>    entry := item.freqElement.Value.(*freqEntry)  <br>    <span class="hljs-built_in">delete</span>(c.items, item.key)  <br>    <span class="hljs-built_in">delete</span>(entry.items, item)  <br>    <span class="hljs-keyword">if</span> isRemovableFreqEntry(entry) &#123;  <br>       c.freqList.Remove(item.freqElement)  <br>    &#125;  <br>    <span class="hljs-keyword">if</span> c.evictedFunc != <span class="hljs-literal">nil</span> &#123;  <br>       c.evictedFunc(item.key, item.value)  <br>    &#125;  <br>&#125;<br></code></pre></td></tr></table></figure><h4 id="LRU"><a href="#LRU" class="headerlink" title="LRU"></a>LRU</h4><p>LRU（最近最少使用）算法会在缓存写满的时候，会根据所有数据的访问记录，淘汰掉未来被访问几率最低的数据。</p><p>阅读源码时发现，其核心逻辑和LFU大体上一致，只不过LRU不用维护访问频率链表，转而维护一个存放缓存对象的双向链表。当获取缓存时，将代表缓存对象的链表对象移动到链表头部。后面开始删除缓存时，获取最后链表的最后一个元素删除即可。</p><p>LRU的列表定义：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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><code class="hljs go"><span class="hljs-comment">// Discards the least recently used items first.  </span><br><span class="hljs-keyword">type</span> LRUCache <span class="hljs-keyword">struct</span> &#123;  <br>    baseCache<br>    <span class="hljs-comment">// 存放key和value的对应关系</span><br>    items     <span class="hljs-keyword">map</span>[<span class="hljs-keyword">interface</span>&#123;&#125;]*list.Element<br>    <span class="hljs-comment">// 存放所有value的链表  </span><br>    evictList *list.List  <br>&#125;<br></code></pre></td></tr></table></figure><p>设置缓存对象的调用逻辑和LFU差不多</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LRUCache)</span></span> set(key, value <span class="hljs-keyword">interface</span>&#123;&#125;) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    <span class="hljs-keyword">var</span> err <span class="hljs-type">error</span>  <br>    <span class="hljs-comment">// 精简代码</span><br>  <br>    <span class="hljs-comment">// Check for existing item  </span><br>    <span class="hljs-keyword">var</span> item *lruItem  <br>    <span class="hljs-keyword">if</span> it, ok := c.items[key]; ok &#123;<br>       <span class="hljs-comment">// 该缓存存在，则将该缓存对应的链表对象，移动到链表的头部  </span><br>       c.evictList.MoveToFront(it)  <br>       item = it.Value.(*lruItem)  <br>       item.value = value  <br>    &#125; <span class="hljs-keyword">else</span> &#123;  <br>       <span class="hljs-comment">// Verify size not exceeded</span><br>       <span class="hljs-comment">// 该缓存对象没有存在</span><br>       <span class="hljs-comment">// 判断当前的缓存池是否已经满了，如果满了则驱逐一个  </span><br>       <span class="hljs-keyword">if</span> c.evictList.Len() &gt;= c.size &#123;  <br>          c.evict(<span class="hljs-number">1</span>)  <br>       &#125;<br>       <span class="hljs-comment">// 构造缓存对象  </span><br>       item = &amp;lruItem&#123;  <br>          clock: c.clock,  <br>          key:   key,  <br>          value: value,  <br>       &#125;<br>       <span class="hljs-comment">// 将该缓存对象移动到头部，然后设置到map中</span><br>       <span class="hljs-comment">// 设置时也代表着该缓存是活跃的，所以会移动到头部。  </span><br>       c.items[key] = c.evictList.PushFront(item)  <br>    &#125;  <br><span class="hljs-comment">// 精简代码</span><br>    <span class="hljs-keyword">return</span> item, <span class="hljs-literal">nil</span>  <br>&#125;<br></code></pre></td></tr></table></figure><p>那驱逐时是怎么驱逐的？这里很简单，直接获取到链表的尾部元素，直接驱逐即可。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// evict removes the oldest item from the cache.func (c *LRUCache) evict(count int) &#123;  </span><br>    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; count; i++ &#123;  <br>       ent := c.evictList.Back()  <br>       <span class="hljs-keyword">if</span> ent == <span class="hljs-literal">nil</span> &#123;  <br>          <span class="hljs-keyword">return</span>  <br>       &#125; <span class="hljs-keyword">else</span> &#123;  <br>          c.removeElement(ent)  <br>       &#125;  <br>    &#125;  <br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LRUCache)</span></span> removeElement(e *list.Element) &#123;  <br>    c.evictList.Remove(e)  <br>    entry := e.Value.(*lruItem)  <br>    <span class="hljs-built_in">delete</span>(c.items, entry.key)  <br>    <span class="hljs-keyword">if</span> c.evictedFunc != <span class="hljs-literal">nil</span> &#123;  <br>       entry := e.Value.(*lruItem)  <br>       c.evictedFunc(entry.key, entry.value)  <br>    &#125;  <br>&#125;<br></code></pre></td></tr></table></figure><p>获取缓存对象</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *LRUCache)</span></span> getValue(key <span class="hljs-keyword">interface</span>&#123;&#125;, onLoad <span class="hljs-type">bool</span>) (<span class="hljs-keyword">interface</span>&#123;&#125;, <span class="hljs-type">error</span>) &#123;  <br>    c.mu.Lock()  <br>    item, ok := c.items[key]<br>    <span class="hljs-comment">// 判断该缓存对象是否已经存在  </span><br>    <span class="hljs-keyword">if</span> ok &#123;  <br>       it := item.Value.(*lruItem)<br>       <span class="hljs-comment">// 判断该缓存对象是否已经到期  </span><br>       <span class="hljs-keyword">if</span> !it.IsExpired(<span class="hljs-literal">nil</span>) &#123;<br>      <span class="hljs-comment">// 没有到期则将链表节点移动到头部并返回  </span><br>          c.evictList.MoveToFront(item)  <br>          v := it.value  <br>          c.mu.Unlock()  <br>          <span class="hljs-keyword">if</span> !onLoad &#123;  <br>             c.stats.IncrHitCount()  <br>          &#125;  <br>          <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>  <br>       &#125;<br>       <span class="hljs-comment">// 该缓存已经过期，删除  </span><br>       c.removeElement(item)  <br>    &#125;  <br>    c.mu.Unlock()  <br>    <span class="hljs-keyword">if</span> !onLoad &#123;  <br>       c.stats.IncrMissCount()  <br>    &#125;<br>    <span class="hljs-comment">// 走到这就意味着缓存已经过期，所以返回该key找不到的错误  </span><br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, KeyNotFoundError  <br>&#125;<br></code></pre></td></tr></table></figure><h2 id="缓存过期时间的实现"><a href="#缓存过期时间的实现" class="headerlink" title="缓存过期时间的实现"></a>缓存过期时间的实现</h2><p>缓存过期时间的实现比较简单，在生成缓存对象的时候会生成过期时间的<code>Time</code>对象，然后在获取缓存的时候，判断该缓存对象的<code>Time</code>对象是否已经过期，过期了就删除该对象并且返回<code>KeyNotFoundErr</code>错误，否则返回该对象。</p><p>从上面章节我们知道，<code>gcache</code>定义了一个缓存接口，包含了许多缓存操作方法，每种缓存驱逐策略都实现了这个接口，每种实现对于缓存过期时间，都是一种方法，所以我就贴出其中一种。下面是LRU的缓存过期时间实现</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><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><code class="hljs go"><span class="hljs-keyword">type</span> lruItem <span class="hljs-keyword">struct</span> &#123;  <br>    clock      Clock  <br>    key        <span class="hljs-keyword">interface</span>&#123;&#125;  <br>    value      <span class="hljs-keyword">interface</span>&#123;&#125;  <br>    expiration *time.Time  <br>&#125;  <br>  <br><span class="hljs-comment">// IsExpired returns boolean value whether this item is expired or not.func (it *lruItem) IsExpired(now *time.Time) bool &#123;  </span><br>    <span class="hljs-keyword">if</span> it.expiration == <span class="hljs-literal">nil</span> &#123;  <br>       <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>  <br>    &#125;  <br>    <span class="hljs-keyword">if</span> now == <span class="hljs-literal">nil</span> &#123;  <br>       t := it.clock.Now()  <br>       now = &amp;t  <br>    &#125;  <br>    <span class="hljs-keyword">return</span> it.expiration.Before(*now)  <br>&#125;<br></code></pre></td></tr></table></figure>]]>
    </content>
    <id>https://sunshinechen.cn/2024/03/13/gcache%EF%BC%8C%E4%B8%80%E4%B8%AA%E7%94%A8go%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%86%85%E5%AD%98%E5%BA%93/</id>
    <link href="https://sunshinechen.cn/2024/03/13/gcache%EF%BC%8C%E4%B8%80%E4%B8%AA%E7%94%A8go%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%86%85%E5%AD%98%E5%BA%93/"/>
    <published>2024-03-13T06:38:03.000Z</published>
    <summary>
      <![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p><code>gcache</code>是一个使用Go语言实现的内存缓存库，在github上有2.5k个star，支持LFU、LRU、ARC等]]>
    </summary>
    <title>gcache-一个用go实现的内存库</title>
    <updated>2026-10-28T09:18:36.414Z</updated>
  </entry>
</feed>
