<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>李斌的技术博客</title>
  
  <subtitle>李斌的技术博客</subtitle>
  <link href="https://blog.mailjob.net/atom.xml" rel="self"/>
  
  <link href="https://blog.mailjob.net/"/>
  <updated>2026-07-25T09:01:29.894Z</updated>
  <id>https://blog.mailjob.net/</id>
  
  <author>
    <name>汀风</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>解剖ClaudeCode的.claude文件夹</title>
    <link href="https://blog.mailjob.net/posts/3694992414.html"/>
    <id>https://blog.mailjob.net/posts/3694992414.html</id>
    <published>2026-04-02T01:31:28.000Z</published>
    <updated>2026-07-25T09:01:29.894Z</updated>
    
    <content type="html"><![CDATA[<h2 id="摘要">摘要</h2><p>文章全面剖析了 Claude Code 的隐藏控制中枢——.claude/ 文件夹。该目录分为项目级和全局级，是定义 Claude 行为、权限和记忆的关键。核心文件 <a href="http://CLAUDE.md">CLAUDE.md</a> 充当指令手册，直接影响系统提示词；rules/ 目录支持模块化和路径感知的指令管理；commands/ 允许创建自定义斜杠命令并执行 shell 脚本；skills/ 和 agents/ 则分别用于自动化工作流和专门的专家角色。此外，文章还介绍了 settings.json 如何通过白名单和黑名单机制管理工具权限，并提供了一套从零开始配置 Claude Code 的渐进式实践路径。</p><h2 id="核心要点">核心要点</h2><ol><li>.claude/ 文件夹是 Claude Code 的行为控制中枢，分为项目级和全局级。</li></ol><p>项目级配置随 Git 提交以实现团队同步，全局配置则存储个人偏好和跨项目的自动记忆。</p><ol start="2"><li><a href="http://CLAUDE.md">CLAUDE.md</a> 是最重要的指令文件，直接决定了 Claude 的工作风格和规范。</li></ol><p>它会被加载到系统提示词中，建议控制在 200 行以内，涵盖构建命令、架构决策和编码规范，避免冗余信息。</p><ol start="3"><li>通过 rules/、commands/ 和 skills/ 实现高度定制化的 AI 协作。</li></ol><p>rules 支持按路径生效的模块化指令，commands 提供手动触发的斜杠命令，而 skills 则是 Claude 根据对话上下文自动调用的工作流。</p><ol start="4"><li>settings.json 建立了安全边界，通过权限控制管理 AI 的工具调用。</li></ol><p>利用 allow 和 deny 列表明确 Claude 可以执行的 Bash 命令和可以读取的文件范围，防止误删或敏感信息泄露。</p><h2 id="金句摘录">金句摘录</h2><blockquote><p>.claude 文件夹是 Claude 在你项目中行为表现的控制中枢。你的指令、自定义命令、权限规则，甚至跨会话的 Claude 记忆，全都保存在这里。</p></blockquote><blockquote><p>你在 <a href="http://CLAUDE.md">CLAUDE.md</a> 中写什么，Claude 就会遵循什么。</p></blockquote><blockquote><p>命令：你输入它们。它们立即运行。技能：Claude 监视你的对话并在相关时触发它们。</p></blockquote><blockquote><p>.claude 文件夹本质上是一套协议，用来告诉 Claude 你是谁、你的项目做什么、它应该遵循什么规则。</p></blockquote><hr><h2 id="正文">正文</h2><p>前言</p><p>一份关于 <a href="http://CLAUDE.md">CLAUDE.md</a>、自定义命令、技能、代理和权限配置的完整指南，手把手教你正确设置。</p><p>大多数 Claude Code 用户将 <code>.claude</code> 文件夹当作黑盒子。他们知道它存在。也见过它出现在项目根目录下，但从来没有打开过，更别提弄明白里面每个文件的用途了。</p><p>这实在是暴殄天物。</p><p><code>.claude</code> 文件夹是 Claude 在你项目中行为表现的控制中枢。你的指令、自定义命令、权限规则，甚至跨会话的 Claude 记忆，全都保存在这里。一旦你搞清楚了什么文件放在哪里、为什么这样放，就能让 Claude Code 完全按照团队的需求来运作。</p><p>本指南将带你逐一剖析这个文件夹的完整结构 —— 从你每天都会用到的文件，到那些设置一次便可高枕无忧的文件。</p><h4 id="两个文件夹，而非一个">两个文件夹，而非一个</h4><p><img src="https://img.github.mailjob.net/202604020940532.jpeg" alt="图像"></p><p>深入讲解之前，有一点值得先说清楚：实际上存在两个 .claude 目录，而不是一个。</p><p>一个位于你的项目内，另一个位于你的用户主目录：</p><ul><li><p>项目级：<code>&lt;your-project&gt;/.claude/</code></p></li><li><p>全局：<code>~/.claude/</code></p></li></ul><p>项目级文件夹包含团队配置。你将其提交到 git。团队中的每个人都获得相同的规则、相同的自定义命令和相同的权限策略。</p><p>全局 <code>~/.claude/</code> 文件夹包含你的个人偏好和本机状态，如会话历史和自动记忆。</p><h4 id="CLAUDE-md：Claude-的指令手册"><a href="http://CLAUDE.md">CLAUDE.md</a>：Claude 的指令手册</h4><p>这是整个系统中最重要的文件。当你启动 Claude Code 会话时，它做的第一件事就是读取 <a href="http://CLAUDE.md">CLAUDE.md</a>，将其直接加载到系统提示词中，并在整个对话过程中时刻参照。</p><p>简单来说：你在 <a href="http://CLAUDE.md">CLAUDE.md</a> 中写什么，Claude 就会遵循什么。</p><p>如果你告诉 Claude 总是在实现之前编写测试，它就会这样做。如果你说 “永远不要使用 console.log 进行错误处理，总是使用自定义日志模块”，它每次都会尊重这一点。</p><p>最常见的做法是在项目根目录放一个 <a href="http://CLAUDE.md">CLAUDE.md</a>。但你也可以在 <code>~/.claude/CLAUDE.md</code> 中设置一个，用于适用于所有项目的全局偏好，甚至可以在子目录中设置一个用于特定文件夹的规则。Claude 会读取所有这些并将它们组合起来。</p><h5 id="CLAUDE-md-中实际应该包含什么"><a href="http://CLAUDE.md">CLAUDE.md</a> 中实际应该包含什么</h5><p>大多数人要么写太多，要么写太少。以下是有效的方法。</p><p><strong>该写的：</strong></p><ul><li><p>构建、测试和 lint 命令（npm run test、make build 等）</p></li><li><p>关键的架构决策（“我们使用 Turborepo 管理 monorepo”）</p></li><li><p>不易察觉的注意事项（“TypeScript 严格模式已开启，未使用的变量是错误”）</p></li><li><p>导入约定、命名模式、错误处理风格</p></li><li><p>主要模块的文件和文件夹结构</p></li></ul><p><strong>不该写的：</strong></p><ul><li><p>任何已经由 linter 或 formatter 配置文件管控的内容</p></li><li><p>可以通过链接查阅的完整文档</p></li><li><p>大段大段解释理论的长篇文字</p></li></ul><p><a href="http://CLAUDE.md">CLAUDE.md</a> 的篇幅建议控制在 200 行以内。超出这个长度会开始大量消耗上下文窗口，Claude 对指令的遵循度反而会下降。</p><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><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="section"># 项目：Acme API</span></span><br><span class="line"> ## 命令</span><br><span class="line"> npm run dev          # 启动开发服务器</span><br><span class="line"> npm run test         # 运行测试（Jest）</span><br><span class="line"> npm run lint         # ESLint + Prettier 检查</span><br><span class="line"> npm run build        # 生产环境构建</span><br><span class="line"> ## 架构</span><br><span class="line"><span class="bullet"> -</span> Express REST API，Node 20</span><br><span class="line"><span class="bullet"> -</span> 通过 Prisma ORM 连接 PostgreSQL</span><br><span class="line"><span class="bullet"> -</span> 所有处理函数位于 src/handlers/</span><br><span class="line"><span class="bullet"> -</span> 共享类型定义位于 src/types/</span><br><span class="line"> ## 规范</span><br><span class="line"><span class="bullet"> -</span> 每个处理函数必须使用 zod 进行请求校验</span><br><span class="line"><span class="bullet"> -</span> 返回值格式统一为 &#123; data, error &#125;</span><br><span class="line"><span class="bullet"> -</span> 绝不向客户端暴露堆栈信息</span><br><span class="line"><span class="bullet"> -</span> 使用 logger 模块，禁止使用 console.log</span><br><span class="line"> ## 注意事项</span><br><span class="line"><span class="bullet"> -</span> 测试使用的是真实的本地数据库，而非 mock。请先运行 <span class="code">`npm run db:test:reset`</span></span><br><span class="line"><span class="bullet"> -</span> TypeScript 严格模式：绝不允许出现未使用的导入</span><br></pre></td></tr></table></figure><p>这大约 20 行。它给了 Claude 在这个代码库中高效工作所需的一切，而无需不断澄清。</p><h4 id="CLAUDE-local-md-用于个人覆盖"><a href="http://CLAUDE.local.md">CLAUDE.local.md</a> 用于个人覆盖</h4><p>有时候你有一些个人偏好，只属于你自己，而非整个团队。也许你更习惯用另一个测试运行器，或者你希望 Claude 始终按某种特定方式打开文件。</p><p>在项目根目录中创建 <a href="http://CLAUDE.local.md">CLAUDE.local.md</a>。Claude 会将其与主 <a href="http://CLAUDE.md">CLAUDE.md</a> 一起读取，并且它会被自动 gitignore，所以你的个人调整永远不会进入仓库。</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></pre></td><td class="code"><pre><span class="line"><span class="section"># 我的个人偏好</span></span><br><span class="line"><span class="bullet">-</span> 总是使用 vitest 而不是 jest</span><br><span class="line"><span class="bullet">-</span> 打开文件时，从 <span class="code">`src/`</span> 目录开始</span><br></pre></td></tr></table></figure><p><img src="https://img.github.mailjob.net/202604020941615.jpeg" alt="图像"></p><h2 id="rules-文件夹：可扩展的模块化指令">rules/ 文件夹：可扩展的模块化指令</h2><p>单个项目用 <a href="http://CLAUDE.md">CLAUDE.md</a> 完全够用。但一旦团队规模扩大，你就会发现 <a href="http://CLAUDE.md">CLAUDE.md</a> 膨胀到了 300 行 —— 没人维护，也没人在意。</p><p><code>rules/</code> 文件夹正是为解决这个问题而生的。</p><p><code>.claude/rules/</code> 目录下的每一个 Markdown 文件，都会随 <a href="http://CLAUDE.md">CLAUDE.md</a> 一起自动加载。与其维护一个臃肿的大文件，不如按关注点拆分指令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">.claude/rules/</span><br><span class="line"> ├── code-style.md</span><br><span class="line"> ├── testing.md</span><br><span class="line"> ├── api-conventions.md</span><br><span class="line"> └── security.md</span><br></pre></td></tr></table></figure><p>每个文件各司其职，更新起来也轻松自如。负责 API 规范的同事编辑 <a href="http://api-conventions.md">api-conventions.md</a>，负责测试标准的同事编辑 <a href="http://testing.md">testing.md</a>，彼此互不干扰。</p><h5 id="路径作用域规则">路径作用域规则</h5><p>真正强大之处在于按路径生效的规则。在规则文件顶部添加 YAML frontmatter 块，该规则就只会在 Claude 处理匹配文件时才激活：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">paths:</span><br><span class="line"><span class="bullet">  -</span> &quot;src/api/<span class="strong">**/<span class="emphasis">*.ts&quot;</span></span></span><br><span class="line"><span class="emphasis"><span class="strong">  - &quot;src/handlers/*</span><span class="emphasis">*/*</span>.ts&quot;</span></span><br><span class="line"><span class="strong">---</span></span><br><span class="line"><span class="strong"># API 设计规范</span></span><br><span class="line"><span class="strong">- 所有处理函数返回 &#123; data, error &#125; 格式</span></span><br><span class="line"><span class="strong">- 使用 zod 校验请求体</span></span><br><span class="line"><span class="strong">- 绝不向客户端暴露内部错误详情</span></span><br></pre></td></tr></table></figure><p>当 Claude 在编辑一个 React 组件时，这个文件根本不会被加载。只有当它在 src/api/ 或 src/handlers/ 目录下工作时才会生效。没有设置 paths 字段的规则则无条件加载，每次会话都会生效。</p><p>一旦你的 <a href="http://CLAUDE.md">CLAUDE.md</a> 开始变得臃肿，这就是正确的演进方向。</p><h4 id="commands-文件夹：你的自定义斜杠命令">commands/ 文件夹：你的自定义斜杠命令</h4><p>Claude Code 开箱即带 <code>/help</code>、<code>/compact</code> 等内置斜杠命令。而 <code>commands/</code> 文件夹让你可以添加自己的命令。</p><p>你放进 <code>.claude/commands/</code> 的每一个 Markdown 文件，都会变成一个斜杠命令。</p><p>名为 <a href="http://review.md">review.md</a> 的文件会生成 <code>/project:review</code> 命令；名为 <a href="http://fix-issue.md">fix-issue.md</a> 的文件会生成 <code>/project:fix-issue</code> 命令。文件名即命令名。</p><p>来看一个简单的例子。创建 <code>.claude/commands/review.md</code>：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line"> description: 合并前审查当前分支的差异，排查问题</span><br><span class="line"> ---</span><br><span class="line"> ## 待审查的变更</span><br><span class="line"> !<span class="code">`git diff --name-only main...HEAD`</span></span><br><span class="line"> ## 详细差异</span><br><span class="line"> !<span class="code">`git diff main...HEAD`</span></span><br><span class="line"> 请审查以上变更，重点关注：</span><br><span class="line"><span class="bullet"> 1.</span> 代码质量问题</span><br><span class="line"><span class="bullet"> 2.</span> 安全漏洞</span><br><span class="line"><span class="bullet"> 3.</span> 测试覆盖缺失</span><br><span class="line"><span class="bullet"> 4.</span> 性能隐患</span><br><span class="line"> 请针对每个文件给出具体、可操作的反馈。</span><br></pre></td></tr></table></figure><p>现在在 Claude Code 中执行 <code>/project:review</code>，它会自动将真实的 git diff 注入提示词，然后再交给 Claude 处理。<code>!</code> 反引号语法会执行 shell 命令并嵌入其输出。这正是让这些命令真正实用 —— 而不仅仅是一段保存好的文本 —— 的关键所在。</p><h5 id="向命令传递参数">向命令传递参数</h5><p>使用 <code>$ARGUMENTS</code> 传递命令名称后的文本：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">description: 调查并修复一个 GitHub issue</span><br><span class="line"><span class="section">argument-hint: [issue-number]</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line">查看本仓库中的 issue #$ARGUMENTS。</span><br><span class="line">!<span class="code">`gh issue view $ARGUMENTS`</span></span><br><span class="line">理解这个 bug，追溯到根本原因，修复它，</span><br><span class="line">并编写一个能捕获该问题的测试。</span><br></pre></td></tr></table></figure><p>执行 <code>/project:fix-issue 234</code>，就会将第 234 号 issue 的内容直接注入提示词。</p><h5 id="个人命令与项目命令">个人命令与项目命令</h5><p><code>.claude/commands/</code> 中的项目命令会被提交到仓库，与团队共享。如果某些命令你希望在所有项目中通用，就把它们放到 <code>~/.claude/commands/</code> 下。这类命令会以 <code>/user:command-name</code> 的形式出现。</p><p>实用的个人命令举例：每日站会助手、按照你的规范生成提交信息的命令，或者快速安全扫描。</p><h4 id="skills-文件夹：按需调用的可复用工作流">skills/ 文件夹：按需调用的可复用工作流</h4><p>前面已经介绍了命令的工作方式。技能（Skills）乍看之下与命令类似，但触发机制有本质区别。在继续深入之前，先把这个区别讲清楚：</p><ul><li><p>命令：你输入它们。它们立即运行。</p></li><li><p>技 *：Claude 监视你的对话并在相关时触发它们。</p></li></ul><p>技能是 Claude 可以自己调用的工作流，无需你输入斜杠命令，只要当前任务与技能的描述相匹配，它就会自动启动。命令等着你来召唤，而技能则在对话中伺机而动，时机一到便主动出击。</p><p>每个技能都有自己的子目录，包含 <a href="http://SKILL.md">SKILL.md</a> 文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">.claude/skills/</span><br><span class="line">├── security-review/</span><br><span class="line">│   ├── SKILL.md</span><br><span class="line">│   └── DETAILED_GUIDE.md</span><br><span class="line">└── deploy/</span><br><span class="line">    ├── SKILL.md</span><br><span class="line">    └── templates/</span><br><span class="line">        └── release-notes.md</span><br></pre></td></tr></table></figure><p><a href="http://SKILL.md">SKILL.md</a> 使用 YAML frontmatter 描述何时使用它：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: security-review</span><br><span class="line">description: 全面的安全审计。当审查代码中的安全漏洞、部署前检查，</span><br><span class="line">  或用户提及安全相关话题时使用。</span><br><span class="line"><span class="section">allowed-tools: Read, Grep, Glob</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line">分析代码库中的安全漏洞：</span><br><span class="line"><span class="bullet">1.</span> SQL 注入与 XSS 风险</span><br><span class="line"><span class="bullet">2.</span> 暴露的凭据或密钥</span><br><span class="line"><span class="bullet">3.</span> 不安全的配置项</span><br><span class="line"><span class="bullet">4.</span> 认证与授权方面的缺陷</span><br><span class="line">报告发现的问题，附上严重等级和具体的修复步骤。</span><br><span class="line">参考 @DETAILED<span class="emphasis">_GUIDE.md 了解我们的安全标准。</span></span><br></pre></td></tr></table></figure><p>当你说 “帮我审查这个 PR 的安全问题” 时，Claude 会读取描述信息，识别出与之匹配，然后自动调用该技能。你也可以通过 <code>/security-review</code> 显式调用。</p><p>与命令的关键区别在于：技能可以捆绑配套文件。上面的 <code>@DETAILED_GUIDE.md</code> 引用的是一份与 <a href="http://SKILL.md">SKILL.md</a> 放在一起的详细文档。命令是单个文件，而技能是一个完整的包。</p><p>个人技能放在 <code>~/.claude/skills/</code> 中，并在你所有项目中可用。</p><h4 id="agents-文件夹：专门的子代理角色">agents/ 文件夹：专门的子代理角色</h4><p>当一项任务复杂到需要一位专职专家来处理时，你可以在 .claude/agents/ 中定义子代理角色。每个代理都是一个 Markdown 文件，拥有自己的系统提示词、工具访问权限和模型偏好：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">.claude/agents/</span><br><span class="line">├── code-reviewer.md</span><br><span class="line">└── security-auditor.md</span><br></pre></td></tr></table></figure><p>这是一个 <a href="http://code-reviewer.md">code-reviewer.md</a> 的样子：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: code-reviewer</span><br><span class="line">description: 资深代码审查专家。在审查 PR、排查 bug 或合并前</span><br><span class="line">  验证实现时主动使用。</span><br><span class="line">model: sonnet</span><br><span class="line"><span class="section">tools: Read, Grep, Glob</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line">你是一位专注于正确性和可维护性的资深代码审查者。</span><br><span class="line">审查代码时：</span><br><span class="line"><span class="bullet">-</span> 指出真正的 bug，而不仅仅是风格问题</span><br><span class="line"><span class="bullet">-</span> 给出具体的修复建议，而非模糊的改进方向</span><br><span class="line"><span class="bullet">-</span> 检查边界情况和错误处理的疏漏</span><br><span class="line"><span class="bullet">-</span> 只在对规模化运行有实际影响时才指出性能问题</span><br></pre></td></tr></table></figure><p>当 Claude 需要进行代码审查时，它会在一个独立的上下文窗口中生成这个代理。代理完成工作后，会压缩整理结论并汇报回来。你的主会话不会被成千上万个中间探索过程的 token 所淹没。</p><p>tools 字段限制了代理的能力范围。安全审计员只需要 Read、Grep 和 Glob，它没有理由去写文件。这种限制是刻意为之的，值得明确声明。</p><p>model 字段则允许你为专项任务使用更经济、更快速的模型。Haiku 可以胜任大多数只读探索工作。把 Sonnet 和 Opus 留给真正需要它们的任务。</p><p>个人代理放在 <code>~/.claude/agents/</code> 下，在你所有的项目中都可以使用。</p><h5 id="自动生成代理">自动生成代理</h5><p>添加 description 字段，Claude 会在任务匹配时自动调用代理：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: Security Auditor</span><br><span class="line">description: 执行全面安全审计</span><br><span class="line">model: sonnet-4</span><br><span class="line"><span class="section">system: 你是一位安全专家。[...]</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><h4 id="settings-json：权限和项目配置">settings.json：权限和项目配置</h4><p><code>.claude/</code> 目录下的 settings.json 文件控制着 Claude 能做什么、不能做什么。你在这里定义 Claude 可以运行哪些工具、可以读取哪些文件，以及在执行某些命令前是否需要先征得你的同意。</p><p>完整文件如下所示：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">   <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://raw.githubusercontent.com/anthropics/claude-code/main/settings-schema.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;allow&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">     <span class="attr">&quot;Bash&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;npm run *&quot;</span><span class="punctuation">,</span> <span class="string">&quot;make *&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Read&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Write&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Edit&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Glob&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Grep&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">   <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;deny&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">     <span class="attr">&quot;Bash&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;rm -rf *&quot;</span><span class="punctuation">,</span> <span class="string">&quot;curl *&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;Read&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;.env&quot;</span><span class="punctuation">,</span> <span class="string">&quot;secrets/**&quot;</span><span class="punctuation">]</span></span><br><span class="line">   <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;context&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">     <span class="attr">&quot;maxFiles&quot;</span><span class="punctuation">:</span> <span class="number">1000</span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;maxLinesPerFile&quot;</span><span class="punctuation">:</span> <span class="number">10000</span></span><br><span class="line">   <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;diff&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">     <span class="attr">&quot;autoStage&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">     <span class="attr">&quot;maxLines&quot;</span><span class="punctuation">:</span> <span class="number">500</span></span><br><span class="line">   <span class="punctuation">&#125;</span></span><br><span class="line"> <span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>以下是每个部分的作用。</p><p><code>$schema</code> 这一行用于在 VS Code 或 Cursor 中启用自动补全和行内校验。务必加上。</p><p>allow 列表包含 Claude 无需请求确认即可直接执行的命令。对于大多数项目，一个合理的白名单应覆盖：</p><ul><li><p><code>Bash(npm run *)</code></p><p>或 <code>Bash(make *)</code>，让 Claude 可以自由运行你的脚本</p></li><li><p><code>Bash(git *)</code></p><p>用于只读的 git 命令</p></li><li><p>Read、Write、Edit、Glob、Grep 用于文件操作</p></li></ul><p>deny 列表包含被彻底禁止的命令，无论如何都不允许执行。一个合理的黑名单应当屏蔽：</p><ul><li><p>破坏性的 shell 命令，如 <code>rm -rf</code></p></li><li><p>直接的网络请求命令，如 curl</p></li><li><p>敏感文件，如 <code>.env</code> 以及 <code>secrets/</code> 目录下的所有内容</p></li><li><p>如果某个命令不在任何一个列表中，Claude 会在执行前先征询你的意见。这种中间地带是刻意设计的 —— 它为你提供了一道安全网，而不必事先穷举所有可能的命令。</p></li></ul><h5 id="settings-local-json-个人权限覆盖">settings.local.json 个人权限覆盖</h5><p>与 <a href="http://CLAUDE.local.md">CLAUDE.local.md</a> 的想法相同。创建 <code>.claude/settings.local.json</code> 用于你不希望提交的权限更改。它是自动 gitignore 的。</p><h4 id="全局～-claude-文件夹">全局～/.claude/ 文件夹</h4><p>你不经常与此文件夹交互，但知道里面有什么是很有用的。</p><p><code>~/.claude/CLAUDE.md</code> 会加载到你所有项目的每一个 Claude Code 会话中。适合放你的个人编程原则、偏好的代码风格，或任何你希望 Claude 记住的内容 —— 无论你当前在哪个仓库中工作。</p><p><code>~/.claude/projects/</code> 按项目存储会话记录和自动记忆。Claude Code 在工作过程中会自动给自己做笔记：它发现的命令、观察到的模式、对架构的洞察。这些内容跨会话持久保存。你可以通过 <code>/memory</code> 命令浏览和编辑它们。</p><p><code>~/.claude/commands/</code> 和 <code>~/.claude/skills/</code> 存放可在所有项目中使用的个人命令和技能。</p><p>通常你不需要手动管理这些文件。但知道它们的存在很有用 —— 比如当 Claude 似乎 “记住” 了你从未告诉过它的东西，或者当你想清除某个项目的自动记忆、从零开始时。</p><h4 id="全景总览">全景总览</h4><p>这就是所有东西如何组合在一起：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">项目根目录:</span><br><span class="line">├── .claude/</span><br><span class="line">│   ├── CLAUDE.md           # 团队指令（已提交）</span><br><span class="line">│   ├── CLAUDE.local.md     # 个人覆盖（已 gitignore）</span><br><span class="line">│   ├── rules/              # 模块化规则（已提交）</span><br><span class="line">│   │   ├── api-conventions.md</span><br><span class="line">│   │   └── testing.md</span><br><span class="line">│   ├── commands/           # 团队斜杠命令（已提交）</span><br><span class="line">│   │   └── review.md</span><br><span class="line">│   ├── skills/             # 团队技能（已提交）</span><br><span class="line">│   │   └── security-review/</span><br><span class="line">│   ├── agents/             # 团队代理（已提交）</span><br><span class="line">│   │   └── code-reviewer.md</span><br><span class="line">│   ├── settings.json       # 权限（已提交）</span><br><span class="line">│   └── settings.local.json # 个人设置（已 gitignore）</span><br><span class="line">└── ...你的项目文件...</span><br><span class="line">全局 (~/.claude/):</span><br><span class="line">├── CLAUDE.md              # 全局偏好</span><br><span class="line">├── commands/              # 个人命令</span><br><span class="line">├── skills/                # 个人技能</span><br><span class="line">├── agents/                # 个人代理</span><br><span class="line">└── projects/              # 自动记忆和会话历史</span><br></pre></td></tr></table></figure><h4 id="从零开始的实践路径">从零开始的实践路径</h4><p>如果你是从头开始配置，以下是一条行之有效的渐进路线。</p><p><strong>步骤 1.</strong> 在 Claude Code 中运行 <code>/init</code>。它通过读取你的项目生成一个启动 <a href="http://CLAUDE.md">CLAUDE.md</a>。将其编辑为基本要素。</p><p><strong>步骤 2.</strong> 添加 <code>.claude/settings.json</code>，配置适合你技术栈的 allow/deny 规则。至少，允许你的运行命令，禁止读取 <code>.env</code> 文件。</p><p><strong>步骤 3.</strong> 为你最常执行的工作流创建一两个命令。代码审查和 issue 修复是很好的起点。</p><p><strong>步骤 4.</strong> 随着项目的成长，当 <a href="http://CLAUDE.md">CLAUDE.md</a> 变得臃肿时，开始将指令拆分到 <code>.claude/rules/</code> 文件中。在合理的场景下，按路径限定其生效范围。</p><p><strong>步骤 5.</strong> 添加 <code>~/.claude/CLAUDE.md</code>，写入你的个人偏好。比如 “永远先写类型定义再写实现” 或 “优先使用函数式风格而非基于类的写法”。</p><p>以上就是 95% 的项目真正需要的全部配置。技能和代理则适用于那些值得打包封装的、反复出现的复杂工作流。</p><h4 id="关键洞察">关键洞察</h4><p><code>.claude</code> 文件夹本质上是一套协议，用来告诉 Claude 你是谁、你的项目做什么、它应该遵循什么规则。你定义得越清晰，花在纠正 Claude 上的时间就越少，它用来做实事的时间就越多。</p><p><a href="http://CLAUDE.md">CLAUDE.md</a> 是你投入产出比最高的文件。先把它写好，其他一切都是锦上添花。</p><p>从小处着手，在实践中不断打磨，把它当作项目基础设施的一部分来对待 —— 就像其他基础设施一样，一旦配置到位，便能日复一日地为你带来回报。</p><p>原文：<a href="https://x.com/akshay_pachaar/status/2035341800739877091">https://x.com/akshay_pachaar/status/2035341800739877091</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;摘要&quot;&gt;摘要&lt;/h2&gt;
&lt;p&gt;文章全面剖析了 Claude Code 的隐藏控制中枢——.claude/ 文件夹。该目录分为项目级和全局级，是定义 Claude 行为、权限和记忆的关键。核心文件 &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.</summary>
      
    
    
    
    <category term="ai" scheme="https://blog.mailjob.net/categories/ai/"/>
    
    
    <category term="ai" scheme="https://blog.mailjob.net/tags/ai/"/>
    
  </entry>
  
  <entry>
    <title>后台权限设计方案</title>
    <link href="https://blog.mailjob.net/posts/960166209.html"/>
    <id>https://blog.mailjob.net/posts/960166209.html</id>
    <published>2025-12-25T03:37:39.000Z</published>
    <updated>2026-07-25T09:01:29.895Z</updated>
    
    <content type="html"><![CDATA[<p>本文是一套 <strong>可落地、可扩展、符合银行/金融系统实践</strong> 的后台权限设计方案，基于 <strong>RBAC（基于角色的访问控制）</strong>，并完整覆盖：</p><ul><li>API 权限</li><li>菜单 / 按钮权限</li><li>数据权限（含 CUSTOM 自定义）</li></ul><h1 id="一、设计背景与目标">一、设计背景与目标</h1><h2 id="1-1-业务背景">1.1 业务背景</h2><p>后台管理系统（银行 / 贷款 / 金融 / 审批类系统）通常具备以下共性：</p><ul><li>多角色、多岗位（管理员、信贷员、风控、负责人等）</li><li><strong>接口一致，但数据可见范围不同</strong></li><li>菜单、按钮、接口权限需要统一管理</li><li>存在跨部门、临时授权、区域授权等复杂场景</li></ul><p>如果没有一套清晰的权限模型，系统将很快出现：</p><ul><li>权限规则混乱</li><li>SQL 到处写 if/else</li><li>数据越权风险</li><li>新需求难以扩展</li></ul><h2 id="1-2-设计目标">1.2 设计目标</h2><p>本方案旨在实现：</p><ol><li><strong>统一、标准的权限模型</strong></li><li><strong>结构清晰、可维护的表设计</strong></li><li><strong>API 作为安全兜底</strong></li><li><strong>灵活但可控的数据权限</strong></li><li><strong>支持复杂业务的自定义授权（CUSTOM）</strong></li></ol><h1 id="二、整体权限模型概览">二、整体权限模型概览</h1><h2 id="2-1-权限模型选型">2.1 权限模型选型</h2><p>采用业内事实标准：</p><blockquote><p><strong>RBAC（Role-Based Access Control）</strong></p></blockquote><p>并在 RBAC 基础上拆分权限层次：</p><ul><li>API 权限（是否能访问接口）</li><li>菜单权限（页面是否可见）</li><li>按钮权限（操作是否可点）</li><li>数据权限（能看到哪些数据行）</li></ul><h2 id="2-2-权限分层思想（非常重要）">2.2 权限分层思想（非常重要）</h2><table><thead><tr><th>层级</th><th>控制内容</th><th>是否真正安全</th><th>说明</th></tr></thead><tbody><tr><td>API 权限</td><td>接口是否可访问</td><td>✅</td><td>后端兜底，必须校验</td></tr><tr><td>菜单权限</td><td>页面是否显示</td><td>❌</td><td>仅用于前端展示</td></tr><tr><td>按钮权限</td><td>按钮是否显示</td><td>❌</td><td>仅用于前端控制</td></tr><tr><td>数据权限</td><td>能看到哪些数据</td><td>✅</td><td>决定 SQL WHERE</td></tr></tbody></table><blockquote><p><strong>真正的安全 = API 权限 + 数据权限</strong><br>菜单 / 按钮只负责“看不看得到”，不负责“能不能做”。</p></blockquote><h1 id="三、数据表-ER-图设计">三、数据表 ER 图设计</h1><h2 id="3-1-表结构与关系">3.1 表结构与关系</h2><pre class="mermaid">erDiagramsys_admin ||--o{ sys_admin_role : "一对多"    sys_role  ||--o{ sys_admin_role : "一对多"    sys_role ||--o{ sys_role_permission : "一对多"    sys_permission ||--o{ sys_role_permission : "一对多"    sys_dept ||--o{ sys_admin : "一对多"    sys_admin {        int id PK "用户ID"        varchar name "姓名"        char password "密码(MD5/加密)"        char mobile "手机号"        int dept_id FK "所属部门ID"        datetime register_time "注册时间"        datetime last_time "最后登录时间"        varchar last_ip "最后登录IP"        tinyint is_delete "是否删除(0否1是)"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }    sys_dept {        int id PK "部门ID"        int parent_id FK "父部门ID"        varchar name "部门名称"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }    sys_role {        int id PK "角色ID"        varchar name "角色名称"        varchar scope "数据权限范围(ALL/DEPT/DEPT_AND_SUB/SELF/CUSTOM)"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }    sys_permission {        int id PK "权限ID"        int parent_id FK "父级权限ID(菜单用)"        tinyint type "权限类型(1接口 2菜单 3按钮)"        varchar name "权限名称"        varchar method "HTTP方法(GET/POST等)"        varchar path "接口或路由路径"        int sort "排序"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }    sys_admin_role {        int id PK "主键ID"        int admin_id FK "后台用户ID"        int role_id FK "角色ID"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }    sys_role_permission {        int id PK "主键ID"        int role_id FK "角色ID"        int permission_id FK "权限ID"        datetime created_at "创建时间"        datetime updated_at "更新时间"    }</pre><h2 id="3-2-表说明速览">3.2 表说明速览</h2><table><thead><tr><th>表名</th><th>作用</th></tr></thead><tbody><tr><td>sys_admin</td><td>后台用户</td></tr><tr><td>sys_dept</td><td>部门组织</td></tr><tr><td>sys_role</td><td>角色（承载权限 + 数据范围）</td></tr><tr><td>sys_permission</td><td>权限（API / 菜单 / 按钮）</td></tr><tr><td>sys_admin_role</td><td>用户-角色关联</td></tr><tr><td>sys_role_permission</td><td>角色-权限关联</td></tr></tbody></table><h1 id="四、RBAC-核心设计说明">四、RBAC 核心设计说明</h1><h2 id="4-1-为什么“用户不直接绑定权限”">4.1 为什么“用户不直接绑定权限”</h2><ul><li>一个用户可能有多个角色</li><li>角色才是业务语义的载体</li><li>权限集中在角色上，便于维护和审计</li></ul><blockquote><p><strong>用户 → 角色 → 权限</strong><br>永远不要：用户 → 权限</p></blockquote><h2 id="4-2-sys-permission-的设计思想">4.2 sys_permission 的设计思想</h2><h3 id="权限类型">权限类型</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1 = API 权限</span><br><span class="line">2 = 菜单权限</span><br><span class="line">3 = 按钮权限</span><br></pre></td></tr></table></figure><h3 id="权限-code-设计原则">权限 code 设计原则</h3><ul><li>前后端统一</li><li>语义清晰</li><li>可复用</li></ul><p>示例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">loan:create</span><br><span class="line">loan:approve</span><br><span class="line">loan:grant</span><br><span class="line">loan:post:list</span><br></pre></td></tr></table></figure><h1 id="五、系统中如何使用这套权限">五、系统中如何使用这套权限</h1><h2 id="5-1-API-权限（中间件，安全兜底）">5.1 API 权限（中间件，安全兜底）</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">PermissionMiddleware</span><span class="params">(r *ghttp.Request)</span></span> &#123;</span><br><span class="line">    user := GetLoginUser(r.Context())</span><br><span class="line"></span><br><span class="line">    ok := permissionSvc.HasApiPermission(</span><br><span class="line">        user.Id,</span><br><span class="line">        r.URL.Path,</span><br><span class="line">        r.Method,</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !ok &#123;</span><br><span class="line">        r.Response.WriteStatusExit(<span class="number">403</span>, <span class="string">&quot;无权限访问&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    r.Middleware.Next()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="核心校验逻辑">核心校验逻辑</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="number">1</span></span><br><span class="line"><span class="keyword">FROM</span> sys_admin_role ur</span><br><span class="line"><span class="keyword">JOIN</span> sys_role_permission rp <span class="keyword">ON</span> ur.role_id <span class="operator">=</span> rp.role_id</span><br><span class="line"><span class="keyword">JOIN</span> sys_permission p <span class="keyword">ON</span> rp.permission_id <span class="operator">=</span> p.id</span><br><span class="line"><span class="keyword">WHERE</span> ur.admin_id <span class="operator">=</span> ?</span><br><span class="line">  <span class="keyword">AND</span> p.type <span class="operator">=</span> <span class="string">&#x27;api&#x27;</span></span><br><span class="line">  <span class="keyword">AND</span> p.path <span class="operator">=</span> ?</span><br><span class="line">  <span class="keyword">AND</span> p.method <span class="operator">=</span> ?</span><br></pre></td></tr></table></figure><h2 id="5-2-菜单权限（登录后返回）">5.2 菜单权限（登录后返回）</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> p.<span class="operator">*</span></span><br><span class="line"><span class="keyword">FROM</span> sys_admin_role ur</span><br><span class="line"><span class="keyword">JOIN</span> sys_role_permission rp <span class="keyword">ON</span> ur.role_id <span class="operator">=</span> rp.role_id</span><br><span class="line"><span class="keyword">JOIN</span> sys_permission p <span class="keyword">ON</span> rp.permission_id <span class="operator">=</span> p.id</span><br><span class="line"><span class="keyword">WHERE</span> ur.admin_id <span class="operator">=</span> ?</span><br><span class="line">  <span class="keyword">AND</span> p.type <span class="operator">=</span> <span class="string">&#x27;menu&#x27;</span></span><br><span class="line"><span class="keyword">ORDER</span> <span class="keyword">BY</span> p.sort;</span><br></pre></td></tr></table></figure><blockquote><p>后端返回菜单树，前端负责渲染。</p></blockquote><h2 id="5-3-按钮权限（前端控制）">5.3 按钮权限（前端控制）</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> p.name</span><br><span class="line"><span class="keyword">FROM</span> sys_admin_role ur</span><br><span class="line"><span class="keyword">JOIN</span> sys_role_permission rp <span class="keyword">ON</span> ur.role_id <span class="operator">=</span> rp.role_id</span><br><span class="line"><span class="keyword">JOIN</span> sys_permission p <span class="keyword">ON</span> rp.permission_id <span class="operator">=</span> p.id</span><br><span class="line"><span class="keyword">WHERE</span> ur.admin_id <span class="operator">=</span> ?</span><br><span class="line">  <span class="keyword">AND</span> p.type <span class="operator">=</span> <span class="string">&#x27;button&#x27;</span>;</span><br></pre></td></tr></table></figure><p>前端示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;v-button v-if=&quot;hasPermission(&#x27;loan:grant&#x27;)&quot; /&gt;</span><br></pre></td></tr></table></figure><h1 id="六、数据权限（Data-Scope）设计">六、数据权限（Data Scope）设计</h1><h2 id="6-1-data-scope-的本质">6.1 data_scope 的本质</h2><blockquote><p><strong>data_scope 决定 SQL 查询时 WHERE 条件如何拼</strong></p></blockquote><p>它只控制：</p><ul><li>能看到哪些数据行</li></ul><p>它不控制：</p><ul><li>接口是否能访问</li><li>页面是否显示</li></ul><h2 id="6-2-标准-data-scope-枚举">6.2 标准 data_scope 枚举</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ALL              全部数据</span><br><span class="line">DEPT             本部门</span><br><span class="line">DEPT_AND_SUB     本部门及子部门</span><br><span class="line">SELF             仅本人</span><br><span class="line">CUSTOM           自定义</span><br></pre></td></tr></table></figure><h2 id="6-3-数据权限只能放在-Service-层">6.3 数据权限只能放在 Service 层</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *LoanService)</span></span> List(ctx context.Context, user *LoginUser) &#123;</span><br><span class="line">    <span class="keyword">switch</span> user.DataScope &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;ALL&quot;</span>:</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;DEPT&quot;</span>:</span><br><span class="line">        <span class="comment">// dept_id = user.DeptId</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;DEPT_AND_SUB&quot;</span>:</span><br><span class="line">        <span class="comment">// dept_id IN (...)</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&quot;SELF&quot;</span>:</span><br><span class="line">        <span class="comment">// creator_id = user.Id</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>数据权限禁止放在中间件或 Controller</strong></p></blockquote><h1 id="七、多角色-data-scope-冲突解决策略">七、多角色 data_scope 冲突解决策略</h1><h2 id="7-1-核心原则">7.1 核心原则</h2><blockquote><p><strong>一个用户，在一个业务场景下，只能有一个最终 data_scope</strong></p></blockquote><h2 id="7-2-推荐策略（银行系统首选）">7.2 推荐策略（银行系统首选）</h2><h3 id="最宽权限优先">最宽权限优先</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ALL &gt; DEPT_AND_SUB &gt; DEPT &gt; CUSTOM &gt; SELF</span><br></pre></td></tr></table></figure><h3 id="示例">示例</h3><table><thead><tr><th>角色组合</th><th>最终 scope</th></tr></thead><tbody><tr><td>DEPT + SELF</td><td>DEPT</td></tr><tr><td>CUSTOM + DEPT</td><td>DEPT</td></tr><tr><td>SELF + CUSTOM</td><td>CUSTOM</td></tr></tbody></table><h2 id="7-3-登录时计算最终-data-scope">7.3 登录时计算最终 data_scope</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">登录 → 查询角色 → 计算 scope → 写入 Session / JWT</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ResolveDataScope</span><span class="params">(roles []Role)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">    priority := <span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>&#123;</span><br><span class="line">        <span class="string">&quot;ALL&quot;</span>: <span class="number">4</span>,</span><br><span class="line">        <span class="string">&quot;DEPT_AND_SUB&quot;</span>: <span class="number">3</span>,</span><br><span class="line">        <span class="string">&quot;DEPT&quot;</span>: <span class="number">2</span>,</span><br><span class="line">        <span class="string">&quot;CUSTOM&quot;</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="string">&quot;SELF&quot;</span>: <span class="number">0</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    max := <span class="number">-1</span></span><br><span class="line">    result := <span class="string">&quot;SELF&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> roles &#123;</span><br><span class="line">        <span class="keyword">if</span> p := priority[r.DataScope]; p &gt; max &#123;</span><br><span class="line">            max = p</span><br><span class="line">            result = r.DataScope</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="八、CUSTOM-自定义数据权限（高级能力）">八、CUSTOM 自定义数据权限（高级能力）</h1><h2 id="8-1-什么时候需要-CUSTOM">8.1 什么时候需要 CUSTOM</h2><ul><li>跨部门授权</li><li>区域/片区管理</li><li>临时审计授权</li><li>精确到具体业务对象</li></ul><h2 id="8-2-自定义数据权限表设计">8.2 自定义数据权限表设计</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> sys_role_data_scope (</span><br><span class="line">  id          <span class="type">BIGINT</span> <span class="keyword">PRIMARY KEY</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;ID&#x27;</span>,</span><br><span class="line">  role_id     <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;角色ID&#x27;</span>,</span><br><span class="line">  scope_type  <span class="type">VARCHAR</span>(<span class="number">32</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;授权类型&#x27;</span>,</span><br><span class="line">  scope_id    <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;授权对象ID&#x27;</span>,</span><br><span class="line">  created_at  DATETIME</span><br><span class="line">) COMMENT<span class="operator">=</span><span class="string">&#x27;角色自定义数据权限表&#x27;</span>;</span><br></pre></td></tr></table></figure><h3 id="scope-type-规范">scope_type 规范</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">DEPT        部门</span><br><span class="line">USER        用户</span><br><span class="line">LOAN        贷款</span><br><span class="line">CUSTOMER    客户</span><br></pre></td></tr></table></figure><h2 id="8-3-CUSTOM-示例">8.3 CUSTOM 示例</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">风控专员角色（CUSTOM）</span><br><span class="line">→ 授权部门：10、12</span><br></pre></td></tr></table></figure><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT INTO</span> sys_role_data_scope (role_id, scope_type, scope_id)</span><br><span class="line"><span class="keyword">VALUES</span></span><br><span class="line">(<span class="number">5</span>, <span class="string">&#x27;DEPT&#x27;</span>, <span class="number">10</span>),</span><br><span class="line">(<span class="number">5</span>, <span class="string">&#x27;DEPT&#x27;</span>, <span class="number">12</span>);</span><br></pre></td></tr></table></figure><h2 id="8-4-CUSTOM-在代码中的生效方式">8.4 CUSTOM 在代码中的生效方式</h2><h3 id="登录态结构">登录态结构</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> LoginUser <span class="keyword">struct</span> &#123;</span><br><span class="line">    Id           <span class="type">int64</span></span><br><span class="line">    DeptId       <span class="type">int64</span></span><br><span class="line">    DataScope    <span class="type">string</span></span><br><span class="line">    CustomScopes <span class="keyword">map</span>[<span class="type">string</span>][]<span class="type">int64</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="加载-CUSTOM-数据">加载 CUSTOM 数据</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">LoadCustomScopes</span><span class="params">(roleIds []<span class="type">int64</span>)</span></span> <span class="keyword">map</span>[<span class="type">string</span>][]<span class="type">int64</span> &#123;</span><br><span class="line">    rows := []<span class="keyword">struct</span> &#123;</span><br><span class="line">        ScopeType <span class="type">string</span></span><br><span class="line">        ScopeId   <span class="type">int64</span></span><br><span class="line">    &#125;&#123;&#125;</span><br><span class="line"></span><br><span class="line">    g.DB().Model(<span class="string">&quot;sys_role_data_scope&quot;</span>).</span><br><span class="line">        WhereIn(<span class="string">&quot;role_id&quot;</span>, roleIds).</span><br><span class="line">        Scan(&amp;rows)</span><br><span class="line"></span><br><span class="line">    result := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>][]<span class="type">int64</span>)</span><br><span class="line">    <span class="keyword">for</span> _, r := <span class="keyword">range</span> rows &#123;</span><br><span class="line">        result[r.ScopeType] = <span class="built_in">append</span>(result[r.ScopeType], r.ScopeId)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="8-5-Service-层统一拼接-SQL">8.5 Service 层统一拼接 SQL</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><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *LoanService)</span></span> applyDataScope(db *gdb.Model, user *LoginUser) *gdb.Model &#123;</span><br><span class="line">    <span class="keyword">switch</span> user.DataScope &#123;</span><br><span class="line">    <span class="keyword">case</span> DataScopeAll:</span><br><span class="line">        <span class="keyword">return</span> db</span><br><span class="line">    <span class="keyword">case</span> DataScopeDept:</span><br><span class="line">        <span class="keyword">return</span> db.Where(<span class="string">&quot;dept_id&quot;</span>, user.DeptId)</span><br><span class="line">    <span class="keyword">case</span> DataScopeDeptAndSub:</span><br><span class="line">        <span class="keyword">return</span> db.WhereIn(<span class="string">&quot;dept_id&quot;</span>, s.deptSvc.GetDeptAndSubIds(user.DeptId))</span><br><span class="line">    <span class="keyword">case</span> DataScopeSelf:</span><br><span class="line">        <span class="keyword">return</span> db.Where(<span class="string">&quot;creator_id&quot;</span>, user.Id)</span><br><span class="line">    <span class="keyword">case</span> DataScopeCustom:</span><br><span class="line">        deptIds := user.CustomScopes[<span class="string">&quot;DEPT&quot;</span>]</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">len</span>(deptIds) == <span class="number">0</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> db.Where(<span class="string">&quot;1=0&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> db.WhereIn(<span class="string">&quot;dept_id&quot;</span>, deptIds)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> db</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="九、总结">九、总结</h1><blockquote><p><strong>这套权限方案的核心思想是：</strong></p></blockquote><ul><li>用 <strong>RBAC</strong> 控制“能不能做”</li><li>用 <strong>API 权限</strong> 作为安全兜底</li><li>用 <strong>data_scope</strong> 控制“能看多少”</li><li>用 <strong>CUSTOM</strong> 解决现实业务中的例外情况</li></ul><p>这是一套 <strong>真正能在银行 / 金融系统中长期演进的后台权限设计方案</strong>。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;本文是一套 &lt;strong&gt;可落地、可扩展、符合银行/金融系统实践&lt;/strong&gt; 的后台权限设计方案，基于 &lt;strong&gt;RBAC（基于角色的访问控制）&lt;/strong&gt;，并完整覆盖：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 权限&lt;/li&gt;
&lt;li&gt;菜单 / 按钮权限&lt;/l</summary>
      
    
    
    
    <category term="blog" scheme="https://blog.mailjob.net/categories/blog/"/>
    
    
    <category term="blog" scheme="https://blog.mailjob.net/tags/blog/"/>
    
  </entry>
  
  <entry>
    <title>解决项目部署CORS跨域问题</title>
    <link href="https://blog.mailjob.net/posts/2084750826.html"/>
    <id>https://blog.mailjob.net/posts/2084750826.html</id>
    <published>2024-10-09T09:17:57.000Z</published>
    <updated>2026-07-25T09:01:29.899Z</updated>
    
    <content type="html"><![CDATA[<h2 id="CORS问题">CORS问题</h2><h3 id="CORS-跨域问题的产生原因">CORS 跨域问题的产生原因</h3><p>CORS（Cross-Origin Resource Sharing）是一种机制，允许从一个域（源）请求来自不同域（源）的资源。现代浏览器出于安全考虑，限制网页的 AJAX 请求，禁止前端脚本访问不同域的资源。这种安全策略称为同源策略（Same-Origin Policy），它限制了不同源（包括协议、域名和端口）的交互。CORS 的出现是为了在特定情况下允许跨源请求，增强了 Web 应用的灵活性。</p><h3 id="如何解决-CORS-跨域问题">如何解决 CORS 跨域问题</h3><p><strong>1. CORS 头部配置</strong>：通过设置 HTTP 响应头来告知浏览器允许跨域请求。常见的头部包括：</p><ul><li><code>Access-Control-Allow-Origin</code>: 指定允许哪些域进行请求，可以是具体的域名或 <code>*</code>（允许所有域）。</li><li><code>Access-Control-Allow-Methods</code>: 指定允许的 HTTP 方法，如 <code>GET</code>、<code>POST</code> 等。</li><li><code>Access-Control-Allow-Headers</code>: 指定允许的请求头。</li><li><code>Access-Control-Max-Age</code>: 指定预检请求的缓存时间。</li></ul><p><strong>2. 代理服务器</strong>：通过设置一个同域的代理服务器，将跨域请求转发到目标域。这种方法常用于开发环境。</p><p><strong>3. JSONP</strong>：通过 <code>&lt;script&gt;</code> 标签的跨域特性，获取数据。此方法仅支持 <code>GET</code> 请求，已经逐渐被 CORS 取代。</p><h3 id="CORS配置详解">CORS配置详解</h3><p>CORS 头部是服务器响应中包含的一组 HTTP 头，用于控制哪些来源的请求被允许，如何处理跨域请求。以下是常见的 CORS 头部配置项及其含义：</p><ol><li>Access-Control-Allow-Origin</li></ol><ul><li><strong>说明</strong>：指定哪些源可以访问资源。</li><li><strong>可配置值</strong>：<ul><li>特定的域名，例如 <code>https://www.example.com</code>，表示仅允许该域的请求。</li><li><code>*</code>，表示允许所有域进行请求（不推荐用于敏感数据）。</li></ul></li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Allow-Origin</span><span class="punctuation">: </span>https://www.example.com</span><br></pre></td></tr></table></figure></li></ul><ol start="2"><li>Access-Control-Allow-Methods</li></ol><ul><li><strong>说明</strong>：指定允许的 HTTP 方法，表示客户端可以使用哪些 HTTP 方法来访问资源。</li><li><strong>可配置值</strong>：可以是任意 HTTP 方法，如 <code>GET</code>、<code>POST</code>、<code>PUT</code>、<code>DELETE</code>、<code>OPTIONS</code> 等，多个方法用逗号分隔。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Allow-Methods</span><span class="punctuation">: </span>GET, POST, OPTIONS</span><br></pre></td></tr></table></figure></li></ul><ol start="3"><li>Access-Control-Allow-Headers</li></ol><ul><li><strong>说明</strong>：指定哪些请求头可以被客户端发送。</li><li><strong>可配置值</strong>：可以是任意请求头，例如 <code>Content-Type</code>、<code>Authorization</code>、<code>X-Custom-Header</code> 等，多个头部用逗号分隔。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Allow-Headers</span><span class="punctuation">: </span>Content-Type, Authorization</span><br></pre></td></tr></table></figure></li></ul><ol start="4"><li>Access-Control-Expose-Headers</li></ol><ul><li><strong>说明</strong>：指定哪些响应头可以被浏览器访问，浏览器默认只允许访问 <code>Cache-Control</code>、<code>Content-Language</code>、<code>Content-Type</code>、<code>Expires</code>、<code>Last-Modified</code> 和 <code>Pragma</code> 等标准响应头。</li><li><strong>可配置值</strong>：可以列出多个响应头。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Expose-Headers</span><span class="punctuation">: </span>X-Custom-Header</span><br></pre></td></tr></table></figure></li></ul><ol start="5"><li>Access-Control-Max-Age</li></ol><ul><li><strong>说明</strong>：指定预检请求的缓存时间（以秒为单位），在此时间内，浏览器可以直接使用相同的请求，而无需再次发送预检请求。</li><li><strong>可配置值</strong>：任意正整数，表示缓存时间。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Max-Age</span><span class="punctuation">: </span>86400  # 24小时</span><br></pre></td></tr></table></figure></li></ul><ol start="6"><li>Access-Control-Allow-Credentials</li></ol><ul><li><strong>说明</strong>：指示是否允许浏览器发送凭据（如 Cookies 和 HTTP 认证信息）到跨域请求的服务器。</li><li><strong>可配置值</strong>：<ul><li><code>true</code>，表示允许发送凭据。</li><li><code>false</code>，表示不允许。</li></ul></li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Allow-Credentials</span><span class="punctuation">: </span>true</span><br></pre></td></tr></table></figure></li></ul><ol start="7"><li>Access-Control-Request-Method</li></ol><ul><li><strong>说明</strong>：在预检请求中使用，指示实际请求所使用的 HTTP 方法。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Request-Method</span><span class="punctuation">: </span>POST</span><br></pre></td></tr></table></figure></li></ul><ol start="8"><li>Access-Control-Request-Headers</li></ol><ul><li><strong>说明</strong>：在预检请求中使用，指示实际请求所使用的请求头。</li><li><strong>示例</strong>：<figure class="highlight http"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">Access-Control-Request-Headers</span><span class="punctuation">: </span>Content-Type, Authorization</span><br></pre></td></tr></table></figure></li></ul><h2 id="不同部署方案CORS问题解决">不同部署方案CORS问题解决</h2><h3 id="同域名下项目部署">同域名下项目部署</h3><p>如果前端和后端共用一个域名（例如 <code>https://www.example.com</code>），则不需要 CORS 的额外配置。前端可以直接访问后端 API，只需确保 API 路由的正确设置。</p><h4 id="前端项目配置">前端项目配置</h4><h5 id="前端项目（Vue-3）">前端项目（Vue 3）</h5><p>在 Vue 3 项目中，你可以直接调用后端 API。例如，使用 <code>axios</code> 发起请求：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">import axios from &#x27;axios&#x27;;</span><br><span class="line"></span><br><span class="line">axios.get(&#x27;/api/data&#x27;)</span><br><span class="line">    .then(response =&gt; &#123;</span><br><span class="line">        console.log(response.data);</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch(error =&gt; &#123;</span><br><span class="line">        console.error(error);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><h5 id="前端项目Nginx配置">前端项目Nginx配置</h5><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span> www.example.com;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 前端项目的配置</span></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="attribute">root</span> /path/to/vue/dist;  <span class="comment"># Vue 生成的静态文件路径</span></span><br><span class="line">        <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>/ /index.html;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 后端 API 的配置</span></span><br><span class="line">    <span class="section">location</span> /api/ &#123;</span><br><span class="line">        <span class="attribute">proxy_pass</span> http://127.0.0.1:8000;  <span class="comment"># 假设 后端项目 运行在 8000 端口</span></span><br><span class="line">        <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 以下省略 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="总结">总结</h5><ol><li><p><strong>前端</strong>（Vue 3）和 <strong>后端</strong>（Laravel）在同一域名下运行，因此不需要 CORS 配置。</p></li><li><p><strong>Nginx</strong> 配置中，前端和后端通过不同的请求路径（如 <code>/</code> 和 <code>/api/</code>）进行区分。所有以 <code>/api/</code> 开头的请求将被代理到后端 Laravel 应用。</p></li></ol><h3 id="不同域名下项目部署">不同域名下项目部署</h3><p>当前端和后端位于不同域名（例如前端 <code>https://www.example.com</code>，后端 <code>https://api.example.com</code>）时，就需要进行 CORS 配置。</p><h4 id="后端项目配置">后端项目配置</h4><h5 id="后端项目（PHP）">后端项目（PHP）</h5><p>在 PHP 后端中，你可以通过设置响应头来解决 CORS：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// index.php</span></span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Access-Control-Allow-Origin: https://www.example.com&#x27;</span>);</span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Access-Control-Allow-Methods: GET, POST, OPTIONS&#x27;</span>); <span class="comment">// 如果运行所有，则配置为 *</span></span><br><span class="line"><span class="title function_ invoke__">header</span>(<span class="string">&#x27;Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization&#x27;</span>);</span><br><span class="line"><span class="comment">// header(&#x27;Content-Type: application/json&#x27;);</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$data</span> = [<span class="string">&#x27;message&#x27;</span> =&gt; <span class="string">&#x27;Hello from API!&#x27;</span>];</span><br><span class="line"><span class="keyword">echo</span> <span class="title function_ invoke__">json_encode</span>(<span class="variable">$data</span>);</span><br></pre></td></tr></table></figure><h5 id="后端项目Nginx配置">后端项目Nginx配置</h5><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span> api.example.com;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="comment"># CORS配置...</span></span><br><span class="line">        <span class="attribute">add_header</span> <span class="string">&#x27;Access-Control-Allow-Origin&#x27;</span> <span class="string">&#x27;https://www.example.com&#x27;</span>; <span class="comment"># 允许前端域名访问</span></span><br><span class="line">        <span class="attribute">add_header</span> <span class="string">&#x27;Access-Control-Allow-Methods&#x27;</span> <span class="string">&#x27;GET, POST, OPTIONS&#x27;</span>;</span><br><span class="line">        <span class="attribute">add_header</span> <span class="string">&#x27;Access-Control-Allow-Headers&#x27;</span> <span class="string">&#x27;Origin, Content-Type, Accept, Authorization&#x27;</span>;</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 处理预检请求</span></span><br><span class="line">        <span class="attribute">if</span> (<span class="variable">$request_method</span> = <span class="string">&#x27;OPTIONS&#x27;</span>) &#123;</span><br><span class="line">            <span class="attribute">add_header</span> <span class="string">&#x27;Access-Control-Max-Age&#x27;</span> <span class="number">86400</span>;</span><br><span class="line">            <span class="attribute">add_header</span> <span class="string">&#x27;Content-Length&#x27;</span> <span class="number">0</span>;</span><br><span class="line">            <span class="attribute">return</span> <span class="number">204</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 其他配置...</span></span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 以下省略 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="前端项目配置-2">前端项目配置</h4><h5 id="前端项目（Vue-3）-2">前端项目（Vue 3）</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">import axios from &#x27;axios&#x27;;</span><br><span class="line"></span><br><span class="line">axios.get(&#x27;https://api.example.com/api.php&#x27;)</span><br><span class="line">    .then(response =&gt; &#123;</span><br><span class="line">        console.log(response.data);</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch(error =&gt; &#123;</span><br><span class="line">        console.error(error);</span><br><span class="line">    &#125;);</span><br></pre></td></tr></table></figure><h5 id="前端项目Nginx配置-2">前端项目Nginx配置</h5><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span> www.example.com;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="attribute">root</span> /path/to/vue/dist;</span><br><span class="line">        <span class="attribute">try_files</span> <span class="variable">$uri</span> <span class="variable">$uri</span>/ /index.html;</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 以下省略 ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结-2">总结</h2><p>CORS 是一种用于解决跨域请求的机制。根据不同的需求，开发者可以选择不同的方案来处理跨域问题：</p><ul><li>同域名的前端和后端之间无需 CORS 配置。</li><li>不同域名之间的请求需要通过设置 CORS 头部来允许访问。</li><li>配置 Nginx 时，要根据请求路径区分前端和后端的处理。</li></ul><p>通过以上配置和理解，开发者可以有效解决跨域问题，实现前后端的顺利交互。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;CORS问题&quot;&gt;CORS问题&lt;/h2&gt;
&lt;h3 id=&quot;CORS-跨域问题的产生原因&quot;&gt;CORS 跨域问题的产生原因&lt;/h3&gt;
&lt;p&gt;CORS（Cross-Origin Resource Sharing）是一种机制，允许从一个域（源）请求来自不同域（源）的资源。现</summary>
      
    
    
    
    <category term="linux" scheme="https://blog.mailjob.net/categories/linux/"/>
    
    
    <category term="php" scheme="https://blog.mailjob.net/tags/php/"/>
    
    <category term="linux" scheme="https://blog.mailjob.net/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>Laravel全局处理数据库异常回滚</title>
    <link href="https://blog.mailjob.net/posts/4100310017.html"/>
    <id>https://blog.mailjob.net/posts/4100310017.html</id>
    <published>2023-07-28T10:51:47.000Z</published>
    <updated>2026-07-25T09:01:29.902Z</updated>
    
    <content type="html"><![CDATA[<p>在 Laravel 应用程序中，我们经常会使用多个数据库连接来处理各种业务需求。然而，当在多个数据库连接中执行操作时，我们可能会遇到操作失败需要回滚的情况。本文将介绍如何使用 Laravel 的异常处理器来实现全局处理 MySQL 回滚问题，并提供了相应的代码示例。</p><h3 id="准备工作">准备工作</h3><p>在开始之前，确保你已经安装了 Laravel 环境并具备基本的 Laravel 开发知识。</p><h3 id="自定义异常处理器">自定义异常处理器</h3><p>首先，我们需要创建一个自定义的异常处理器类。这个自定义类需要实现 <code>Illuminate\Contracts\Debug\ExceptionHandler</code> 接口，以便能够捕获和处理异常。以下是一个简单的示例代码：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Exceptions</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Exception</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Contracts</span>\<span class="title">Debug</span>\<span class="title">ExceptionHandler</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Foundation</span>\<span class="title">Exceptions</span>\<span class="title">Handler</span> <span class="keyword">as</span> <span class="title">ExceptionHandler</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">DB</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomExceptionHandler</span> <span class="keyword">extends</span> <span class="title">ExceptionHandler</span> <span class="keyword">implements</span> <span class="title">ExceptionHandler</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$e</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$e</span> <span class="keyword">instanceof</span> \PDOException) &#123;</span><br><span class="line">            <span class="comment">// 获取所有已经配置的数据库连接</span></span><br><span class="line">            <span class="variable">$connections</span> = <span class="title function_ invoke__">config</span>(<span class="string">&#x27;database.connections&#x27;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">foreach</span> (<span class="variable">$connections</span> <span class="keyword">as</span> <span class="variable">$connection</span> =&gt; <span class="variable">$config</span>) &#123;</span><br><span class="line">                <span class="comment">// 判断数据库连接是否启用了事务</span></span><br><span class="line">                <span class="keyword">if</span> (<span class="variable">$config</span>[<span class="string">&#x27;transaction&#x27;</span>]) &#123;</span><br><span class="line">                    <span class="comment">// 回滚数据库操作</span></span><br><span class="line">                    DB::<span class="title function_ invoke__">connection</span>(<span class="variable">$connection</span>)-&gt;<span class="title function_ invoke__">rollBack</span>();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 这里可以根据业务需求进行其他的处理操作</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> <span class="title function_ invoke__">response</span>()-&gt;<span class="title function_ invoke__">json</span>([</span><br><span class="line">                <span class="string">&#x27;message&#x27;</span> =&gt; <span class="string">&#x27;数据库操作失败&#x27;</span>,</span><br><span class="line">                <span class="string">&#x27;error&#x27;</span> =&gt; <span class="variable">$e</span>-&gt;<span class="title function_ invoke__">getMessage</span>(),</span><br><span class="line">            ], <span class="number">500</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$e</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述代码中，我们首先判断是否抛出的异常是 <code>PDOException</code> 类型的异常。如果是，我们获取所有已经配置的数据库连接，并遍历每个连接判断是否启用了事务。如果启用了事务，我们使用相应的连接对象执行回滚操作。</p><p>然后，我们可以根据实际情况在处理器中添加其他需要的处理操作。在示例代码中，我们返回一个 JSON 响应，包含自定义的错误信息和异常消息。</p><h3 id="注册异常处理器">注册异常处理器</h3><p>接下来，我们需要将自定义的异常处理器注册到 Laravel 框架中。打开 <code>app/Exceptions/Handler.php</code> 文件并修改 <code>register</code> 方法如下：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">register</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">$this</span>-&gt;app-&gt;<span class="title function_ invoke__">environment</span>(<span class="string">&#x27;production&#x27;</span>)) &#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">reportable</span>(function (<span class="built_in">Throwable</span> <span class="variable">$e</span>) &#123;</span><br><span class="line">            <span class="comment">//</span></span><br><span class="line">        &#125;);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">renderable</span>(function (<span class="built_in">Throwable</span> <span class="variable">$e</span>) &#123;</span><br><span class="line">            <span class="comment">// 使用自定义的异常处理器</span></span><br><span class="line">            <span class="keyword">return</span> <span class="title function_ invoke__">resolve</span>(<span class="title class_">\App\Exceptions\CustomExceptionHandler</span>::<span class="variable language_">class</span>)-&gt;<span class="title function_ invoke__">render</span>(<span class="title function_ invoke__">request</span>(), <span class="variable">$e</span>);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述代码中，我们调用了 <code>renderable</code> 方法并传递一个回调函数进去。这个回调函数会在异常发生时被调用，并使用 <code>resolve</code> 函数来获取我们自定义的异常处理器实例，然后调用其 <code>render</code> 方法来处理异常。</p><p>请确保根据实际路径修改命名空间和类名。</p><h3 id="总结">总结</h3><p>通过以上步骤，我们成功地实现了在 Laravel 框架中处理多个数据库连接的 MySQL 回滚问题。我们创建了一个自定义的异常处理器，通过遍历所有配置的数据库连接，如果启用了事务，就执行回滚操作。同时，我们还可以在处理器中添加其他的处理操作以满足实际业务需求。</p><p>以上就是本文介绍的方法，希望能对你解决多个数据库连接下的 MySQL 回滚问题有所帮助。</p><h3 id="可能的优化点">可能的优化点</h3><p>除了以上方法，还有一些可能的优化点可以考虑：</p><ol><li><strong>异常处理细化</strong>：根据不同的异常类型，可以采取不同的处理策略。例如，某些异常可能只需要回滚特定的数据库连接，而不是所有连接。</li><li><strong>错误日志记录</strong>：将异常信息记录到日志文件中，方便排查问题和追踪。</li><li><strong>通知机制</strong>：在发生异常时，可以发送通知给开发团队或相关人员，以便及时处理问题。</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 Laravel 应用程序中，我们经常会使用多个数据库连接来处理各种业务需求。然而，当在多个数据库连接中执行操作时，我们可能会遇到操作失败需要回滚的情况。本文将介绍如何使用 Laravel 的异常处理器来实现全局处理 MySQL 回滚问题，并提供了相应的代码示例。&lt;/p&gt;</summary>
      
    
    
    
    <category term="php" scheme="https://blog.mailjob.net/categories/php/"/>
    
    
    <category term="php" scheme="https://blog.mailjob.net/tags/php/"/>
    
  </entry>
  
  <entry>
    <title>使用go-redis/redismock做Redis单元测试</title>
    <link href="https://blog.mailjob.net/posts/3307624659.html"/>
    <id>https://blog.mailjob.net/posts/3307624659.html</id>
    <published>2023-07-26T01:17:50.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<p>在开发应用程序时，使用 Redis 是一种常见的方式来处理和存储数据。Redis 提供了五种常用的数据类型：String、List、Set、Hash 和 Sorted Set。除了这些数据类型之外，有时我们还需要在 Redis 中执行复杂的命令序列，这时可以使用 Lua 脚本来实现。在本篇博客中，我们将探讨如何在 Go 语言中使用 <code>go-redis/redismock</code> 来测试 Redis 常用的五种数据类型以及执行 Lua 脚本。</p><h3 id="1-简介">1. 简介</h3><p>首先，让我们了解一下 <code>go-redis/redismock</code> 是什么。它是一个用于 Mock Redis 数据库的库，可以模拟 Redis 客户端的行为，无需实际连接到真实的 Redis 数据库。这样，我们可以在单元测试中更快速和可控地测试与 Redis 交互的代码。</p><h3 id="2-安装和设置">2. 安装和设置</h3><p>在开始之前，请确保您已经安装了 Go 环境，并配置好了 Go 的工作空间。</p><p>首先，我们需要安装 <code>go-redis/redismock</code> 和 <code>go-redis/redis/v8</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/go-redis/redismock/v8</span><br><span class="line">go get github.com/go-redis/redis/v8</span><br></pre></td></tr></table></figure><h3 id="3-示例：测试-Redis-String、List、Set、Hash-和-Sorted-Set">3. 示例：测试 Redis String、List、Set、Hash 和 Sorted Set</h3><h4 id="String-类型">String 类型</h4><p>首先，我们将测试 Redis 中的 String 类型。我们创建一个 <code>string_ops.go</code> 文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// string_ops.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// SetString 设置 Redis 中的 String 类型数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetString</span><span class="params">(client *redis.Client, key, value <span class="type">string</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line"><span class="keyword">return</span> client.Set(context.Background(), key, value, <span class="number">0</span>).Err()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GetString 获取 Redis 中的 String 类型数据</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetString</span><span class="params">(client *redis.Client, key <span class="type">string</span>)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.Get(context.Background(), key).Result()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>SetString</code> 和 <code>GetString</code> 函数，用于设置和获取 Redis 中的 String 类型数据。</p><p>接下来，我们编写单元测试来测试这两个函数。创建一个 <code>string_ops_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// string_ops_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestSetAndGet_String</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;mykey&quot;</span></span><br><span class="line">value := <span class="string">&quot;myvalue&quot;</span></span><br><span class="line"></span><br><span class="line">mock.ExpectSet(key, value).SetVal(<span class="string">&quot;OK&quot;</span>)</span><br><span class="line">mock.ExpectGet(key).SetVal(value)</span><br><span class="line"></span><br><span class="line">err := SetString(client, key, value)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;SetString failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">result, err := GetString(client, key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;GetString failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> result != value &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected value, got: %s, want: %s&quot;</span>, result, value)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="List-类型">List 类型</h4><p>接下来，我们测试 Redis 中的 List 类型。我们创建一个 <code>list_ops.go</code> 文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// list_ops.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// ListPush 在 Redis 中的 List 类型数据上进行 push 操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ListPush</span><span class="params">(client *redis.Client, key <span class="type">string</span>, values ...<span class="type">string</span>)</span></span> (<span class="type">int64</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.LPush(context.Background(), key, values).Result()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ListPop 在 Redis 中的 List 类型数据上进行 pop 操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ListPop</span><span class="params">(client *redis.Client, key <span class="type">string</span>)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.LPop(context.Background(), key).Result()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>ListPush</code> 和 <code>ListPop</code> 函数，用于在 Redis 中添加和弹出 List 类型数据。</p><p>接下来，我们编写单元测试来测试这两个函数。创建一个 <code>list_ops_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// list_ops_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestListPushAndPop</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;mylist&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 ListPush</span></span><br><span class="line">values := []<span class="type">string</span>&#123;<span class="string">&quot;item1&quot;</span>, <span class="string">&quot;item2&quot;</span>, <span class="string">&quot;item3&quot;</span>&#125;</span><br><span class="line">mock.ExpectLPush(key, values).SetVal(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 ListPop</span></span><br><span class="line">mock.ExpectLPop(key).SetVal(<span class="string">&quot;item3&quot;</span>)</span><br><span class="line"></span><br><span class="line">count, err := ListPush(client, key, values...)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;ListPush failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> count != <span class="number">3</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected count, got: %d, want: %d&quot;</span>, count, <span class="number">3</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">item, err := ListPop(client, key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;ListPop failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> item != <span class="string">&quot;item3&quot;</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected item, got: %s, want: %s&quot;</span>, item, <span class="string">&quot;item3&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Set-类型">Set 类型</h4><p>接下来，我们测试 Redis 中的 Set 类型。我们创建一个 <code>set_ops.go</code> 文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// set_ops.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// SetAdd 在 Redis 中的 Set 类型数据上进行 add 操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetAdd</span><span class="params">(client *redis.Client, key <span class="type">string</span>, members ...<span class="type">string</span>)</span></span> (<span class="type">int64</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.SAdd(context.Background(), key, members).Result()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// SetMembers 获取 Redis 中的 Set 类型数据的所有成员</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SetMembers</span><span class="params">(client *redis.Client, key <span class="type">string</span>)</span></span> ([]<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.SMembers(context.Background(), key).Result()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>SetAdd</code> 和 <code>SetMembers</code> 函数，用于在 Redis 中添加和获取 Set 类型数据。</p><p>接下来，我们编写单元测试来测试这两个函数。创建一个 <code>set_ops_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// set_ops_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestSetAddAndMembers</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;myset&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 SetAdd</span></span><br><span class="line">members := []<span class="type">string</span>&#123;<span class="string">&quot;member1&quot;</span>, <span class="string">&quot;member2&quot;</span>, <span class="string">&quot;member3&quot;</span>&#125;</span><br><span class="line">mock.ExpectSAdd(key, members).SetVal(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 SetMembers</span></span><br><span class="line">mock.ExpectSMembers(key).SetVal([]<span class="type">string</span>&#123;<span class="string">&quot;member1&quot;</span>, <span class="string">&quot;member2&quot;</span>, <span class="string">&quot;member3&quot;</span>&#125;)</span><br><span class="line"></span><br><span class="line">count, err := SetAdd(client, key, members...)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;SetAdd failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> count != <span class="number">3</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected count, got: %d, want: %d&quot;</span>, count, <span class="number">3</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">result, err := SetMembers(client, key)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;SetMembers failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> !reflect.DeepEqual(result, members) &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected members, got: %v, want: %v&quot;</span>, result, members)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Hash-类型">Hash 类型</h4><p>接下来，我们测试 Redis 中的 Hash 类型。我们创建一个 <code>hash_ops.go</code> 文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// hash_ops.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// HashSet 在 Redis 中的 Hash 类型数据上进行设置操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">HashSet</span><span class="params">(client *redis.Client, key <span class="type">string</span>, fieldValues <span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="type">error</span> &#123;</span><br><span class="line"><span class="keyword">return</span> client.HMSet(context.Background(), key, fieldValues).Err()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HashGet 获取 Redis 中的 Hash 类型数据的指定字段值</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">HashGet</span><span class="params">(client *redis.Client, key, field <span class="type">string</span>)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.HGet(context.Background(), key, field).Result()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>HashSet</code> 和 <code>HashGet</code> 函数，用于在 Redis 中设置和获取 Hash 类型数据。</p><p>接下来，我们编写单元测试来测试这两个函数。创建一个 <code>hash_ops_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// hash_ops_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestHashSetAndGet</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;myhash&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 HashSet</span></span><br><span class="line">fieldValues := <span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">interface</span>&#123;&#125;&#123;</span><br><span class="line"><span class="string">&quot;field1&quot;</span>: <span class="string">&quot;value1&quot;</span>,</span><br><span class="line"><span class="string">&quot;field2&quot;</span>: <span class="string">&quot;value2&quot;</span>,</span><br><span class="line"><span class="string">&quot;field3&quot;</span>: <span class="string">&quot;value3&quot;</span>,</span><br><span class="line">&#125;</span><br><span class="line">mock.ExpectHMSet(key, fieldValues).SetVal(<span class="string">&quot;OK&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 HashGet</span></span><br><span class="line">field := <span class="string">&quot;field2&quot;</span></span><br><span class="line">mock.ExpectHGet(key, field).SetVal(<span class="string">&quot;value2&quot;</span>)</span><br><span class="line"></span><br><span class="line">err := HashSet(client, key, fieldValues)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;HashSet failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">result, err := HashGet(client, key, field)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;HashGet failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> result != <span class="string">&quot;value2&quot;</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected value, got: %s, want: %s&quot;</span>, result, <span class="string">&quot;value2&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Sorted-Set-类型">Sorted Set 类型</h4><p>最后，我们测试 Redis 中的 Sorted Set 类型。我们创建一个 <code>sorted_set_ops.go</code> 文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sorted_set_ops.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// SortedSetAdd 在 Redis 中的 Sorted Set 类型数据上进行 add 操作</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SortedSetAdd</span><span class="params">(client *redis.Client, key <span class="type">string</span>, members ...*redis.Z)</span></span> (<span class="type">int64</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.ZAdd(context.Background(), key, members...).Result()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// SortedSetRange 获取 Redis 中的 Sorted Set 类型数据指定范围的成员</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SortedSetRange</span><span class="params">(client *redis.Client, key <span class="type">string</span>, start, stop <span class="type">int64</span>)</span></span> ([]<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> client.ZRange(context.Background(), key, start, stop).Result()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>SortedSetAdd</code> 和 <code>SortedSetRange</code> 函数，用于在 Redis 中添加和获取 Sorted Set 类型数据。</p><p>接下来，我们编写单元测试来测试这两个函数。创建一个 <code>sorted_set_ops_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sorted_set_ops_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;reflect&quot;</span></span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestSortedSetAddAndRange</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;mysortedset&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 SortedSetAdd</span></span><br><span class="line">members := []*redis.Z&#123;</span><br><span class="line">&amp;redis.Z&#123;Score: <span class="number">1</span>, Member: <span class="string">&quot;member1&quot;</span>&#125;,</span><br><span class="line">&amp;redis.Z&#123;Score: <span class="number">2</span>, Member: <span class="string">&quot;member2&quot;</span>&#125;,</span><br><span class="line">&amp;redis.Z&#123;Score: <span class="number">3</span>, Member: <span class="string">&quot;member3&quot;</span>&#125;,</span><br><span class="line">&#125;</span><br><span class="line">mock.ExpectZAdd(key, members...).SetVal(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试 SortedSetRange</span></span><br><span class="line">mock.ExpectZRange(key, <span class="number">0</span>, <span class="number">-1</span>).SetVal([]<span class="type">string</span>&#123;<span class="string">&quot;member1&quot;</span>, <span class="string">&quot;member2&quot;</span>, <span class="string">&quot;member3&quot;</span>&#125;)</span><br><span class="line"></span><br><span class="line">count, err := SortedSetAdd(client, key, members...)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;SortedSetAdd failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> count != <span class="number">3</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected count, got: %d, want: %d&quot;</span>, count, <span class="number">3</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">result, err := SortedSetRange(client, key, <span class="number">0</span>, <span class="number">-1</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;SortedSetRange failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">expected := []<span class="type">string</span>&#123;<span class="string">&quot;member1&quot;</span>, <span class="string">&quot;member2&quot;</span>, <span class="string">&quot;member3&quot;</span>&#125;</span><br><span class="line"><span class="keyword">if</span> !reflect.DeepEqual(result, expected) &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected members, got: %v, want: %v&quot;</span>, result, expected)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-执行-Lua-脚本">4. 执行 Lua 脚本</h3><p>现在，让我们测试在 Redis 中执行 Lua 脚本。我们将使用 <code>counter.lua</code> 脚本，并在其中实现计数器应用。</p><p>首先，创建一个名为 <code>counter.lua</code> 的 Lua 脚本：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- counter.lua</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> key = KEYS[<span class="number">1</span>]</span><br><span class="line"><span class="keyword">local</span> increment = <span class="built_in">tonumber</span>(ARGV[<span class="number">1</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> increment &gt; <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&#x27;INCRBY&#x27;</span>, key, increment)</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&#x27;GET&#x27;</span>, key)</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>在该脚本中，我们接收一个键和一个增量值作为参数。如果增量值大于 0，则在 Redis 中使用 <code>INCRBY</code> 命令增加计数；否则，返回该键的当前值。</p><p>接下来，我们创建一个名为 <code>counter.go</code> 的文件，并实现相应的 Go 代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// counter.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;context&quot;</span></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redis/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">//  Lua 脚本用于增加计数或获取当前值</span></span><br><span class="line"><span class="keyword">const</span> counterLuaScript = <span class="string">`</span></span><br><span class="line"><span class="string">    local key = KEYS[1]</span></span><br><span class="line"><span class="string">    local increment = tonumber(ARGV[1])</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    if increment &gt; 0 then</span></span><br><span class="line"><span class="string">        return redis.call(&#x27;INCRBY&#x27;, key, increment)</span></span><br><span class="line"><span class="string">    else</span></span><br><span class="line"><span class="string">        return redis.call(&#x27;GET&#x27;, key)</span></span><br><span class="line"><span class="string">    end</span></span><br><span class="line"><span class="string">`</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// IncreaseCounter 使用 Redis EVAL 命令执行 Lua 脚本，增加计数或获取当前值</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IncreaseCounter</span><span class="params">(client *redis.Client, key <span class="type">string</span>, increment <span class="type">int64</span>)</span></span> (<span class="type">int64</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="comment">// 调用 Redis EVAL 命令执行 Lua 脚本</span></span><br><span class="line">result, err := client.Eval(context.Background(), counterLuaScript, []<span class="type">string</span>&#123;key&#125;, increment).Result()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 转换结果为整数并返回</span></span><br><span class="line"><span class="keyword">if</span> intValue, ok := result.(<span class="type">int64</span>); ok &#123;</span><br><span class="line"><span class="keyword">return</span> intValue, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>, fmt.Errorf(<span class="string">&quot;unexpected result type&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这个示例中，我们编写了 <code>IncreaseCounter</code> 函数，它使用 Redis 客户端执行我们之前创建的 Lua 脚本，并返回结果。</p><p>接下来，我们编写单元测试来测试 <code>IncreaseCounter</code> 函数。创建一个 <code>counter_test.go</code> 文件：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// counter_test.go</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;testing&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/go-redis/redismock/v8&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestIncreaseCounter_PositiveIncrement</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;mykey&quot;</span></span><br><span class="line">increment := <span class="type">int64</span>(<span class="number">5</span>)</span><br><span class="line"></span><br><span class="line">mock.ExpectEval(counterLuaScript, []<span class="type">string</span>&#123;key&#125;, increment).SetVal(increment)</span><br><span class="line"></span><br><span class="line">result, err := IncreaseCounter(client, key, increment)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;IncreaseCounter failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> result != increment &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected result, got: %d, want: %d&quot;</span>, result, increment)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestIncreaseCounter_NegativeIncrement</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, mock := redismock.NewClientMock()</span><br><span class="line"></span><br><span class="line">key := <span class="string">&quot;mykey&quot;</span></span><br><span class="line">increment := <span class="type">int64</span>(<span class="number">-5</span>)</span><br><span class="line">currentValue := <span class="type">int64</span>(<span class="number">10</span>)</span><br><span class="line"></span><br><span class="line">mock.ExpectEval(counterLuaScript, []<span class="type">string</span>&#123;key&#125;, increment).SetVal(currentValue)</span><br><span class="line"></span><br><span class="line">result, err := IncreaseCounter(client, key, increment)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;IncreaseCounter failed: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> result != currentValue &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unexpected result, got: %d, want: %d&quot;</span>, result, currentValue)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := mock.ExpectationsWereMet(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">t.Errorf(<span class="string">&quot;unfulfilled expectations: %s&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-结论">5. 结论</h3><p>在本文中，我们学习了如何使用 <code>go-redis/redismock</code> 在 Go 语言中测试 Redis 常用的五种数据类型以及执行 Lua 脚本。我们测试了 Redis 中的 String、List、Set、Hash 和 Sorted Set 类型，并实现了一个简单的计数器应用来测试执行 Lua 脚本的功能。通过单元测试，我们可以确保这些 Redis 操作的正确性和稳定性。使用 <code>go-redis/redismock</code>，我们可以在不连接到真实 Redis 数据库的情况下快速和可控地测试与 Redis 交互的代码。这为我们的应用程序开发和维护带来了便利和信心。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在开发应用程序时，使用 Redis 是一种常见的方式来处理和存储数据。Redis 提供了五种常用的数据类型：String、List、Set、Hash 和 Sorted Set。除了这些数据类型之外，有时我们还需要在 Redis 中执行复杂的命令序列，这时可以使用 Lua 脚</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
    <category term="redis" scheme="https://blog.mailjob.net/tags/redis/"/>
    
  </entry>
  
  <entry>
    <title>Laravel项目Exception优化最佳实践</title>
    <link href="https://blog.mailjob.net/posts/194135748.html"/>
    <id>https://blog.mailjob.net/posts/194135748.html</id>
    <published>2023-07-05T05:16:04.000Z</published>
    <updated>2026-07-25T09:01:29.902Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言">引言</h2><p>全局异常处理是一种关键的开发实践，可以帮助我们更好地处理应用程序中的异常情况。本文将介绍如何在 Laravel 中实现全局异常处理，并探讨一些最佳实践，包括日志记录、异常监控和报警以及单元测试。</p><p>在开发 Web 应用程序时，异常处理是至关重要的。当应用程序发生异常时，我们希望能够及时捕获和处理异常，并提供有用的错误信息给用户或开发团队。在 Laravel 框架中，我们可以通过全局异常处理来统一处理应用程序中的异常情况。本文将介绍如何在 Laravel 中实现全局异常处理，并分享一些最佳实践。</p><h2 id="实现步骤">实现步骤</h2><h3 id="1-创建自定义异常处理器类：">1. 创建自定义异常处理器类：</h3><p>创建一个自定义的异常处理器类，用于处理应用程序中的异常。可以在 <code>app/Exceptions</code> 目录下创建一个新的异常处理器类，例如 <code>CustomExceptionHandler.php</code>。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Exceptions</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Exception</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Foundation</span>\<span class="title">Exceptions</span>\<span class="title">Handler</span> <span class="keyword">as</span> <span class="title">ExceptionHandler</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomExceptionHandler</span> <span class="keyword">extends</span> <span class="title">ExceptionHandler</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">  <span class="comment">// 自定义数据</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="variable">$data</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params"><span class="variable">$message</span>, <span class="variable">$code</span>, <span class="variable">$data</span> = <span class="literal">null</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="built_in">parent</span>::<span class="title function_ invoke__">__construct</span>(<span class="variable">$message</span>, <span class="variable">$code</span>);</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;data = <span class="variable">$data</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="comment">// 自定义异常处理逻辑</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">      <span class="comment">// 在这里可以将自定义数据记录到日志或其他地方</span></span><br><span class="line">        <span class="comment">// 可以使用 $this-&gt;data 访问自定义数据</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-注册自定义异常处理器：">2. 注册自定义异常处理器：</h3><p>打开 <code>app/Exceptions/Handler.php</code> 文件，并将 <code>report</code> 和 <code>render</code> 方法中的异常处理逻辑迁移到自定义异常处理器中。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Exceptions</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Exception</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Foundation</span>\<span class="title">Exceptions</span>\<span class="title">Handler</span> <span class="keyword">as</span> <span class="title">ExceptionHandler</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Handler</span> <span class="keyword">extends</span> <span class="title">ExceptionHandler</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$dontReport</span> = [</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    ];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$dontFlash</span> = [</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    ];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">register</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">reportable</span>(function (<span class="built_in">Exception</span> <span class="variable">$exception</span>) &#123;</span><br><span class="line">            <span class="comment">//</span></span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">shouldReport</span>(<span class="variable">$exception</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="title function_ invoke__">app</span>(<span class="title class_">CustomExceptionHandler</span>::<span class="variable language_">class</span>)-&gt;<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-创建异常处理器中间件：">3. 创建异常处理器中间件：</h3><p>创建一个异常处理器中间件，用于在全局范围内处理异常。可以在 <code>app/Http/Middleware</code> 目录下创建一个新的中间件类，例如 <code>HandleExceptions.php</code>。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Http</span>\<span class="title class_">Middleware</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Closure</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Contracts</span>\<span class="title">Debug</span>\<span class="title">ExceptionHandler</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HandleExceptions</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$handler</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span>(<span class="params">ExceptionHandler <span class="variable">$handler</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable language_">$this</span>-&gt;handler = <span class="variable">$handler</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">handle</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Closure</span> <span class="variable">$next</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">$this</span>-&gt;handler-&gt;<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$next</span>(<span class="variable">$request</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-注册异常处理器中间件：">4. 注册异常处理器中间件：</h3><p>打开 <code>app/Http/Kernel.php</code> 文件，并将异常处理器中间件添加到 <code>$middleware</code> 数组中。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Http</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Foundation</span>\<span class="title">Http</span>\<span class="title">Kernel</span> <span class="keyword">as</span> <span class="title">HttpKernel</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Kernel</span> <span class="keyword">extends</span> <span class="title">HttpKernel</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$middleware</span> = [</span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">        <span class="title class_">\App\Http\Middleware\HandleExceptions</span>::<span class="variable language_">class</span>,</span><br><span class="line">    ];</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-日志记录：">5. 日志记录：</h3><p>Laravel 默认已经配置了日志记录。确保在 <code>config/logging.php</code> 文件中的 <code>channels</code> 配置中有一个适当的日志通道，以记录异常信息。</p><p>在 <code>config/logging.php</code> 文件中，可以添加一个新的日志通道，例如 <code>exceptions</code>：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;channels&#x27;</span> =&gt; [</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="string">&#x27;exceptions&#x27;</span> =&gt; [</span><br><span class="line">        <span class="string">&#x27;driver&#x27;</span> =&gt; <span class="string">&#x27;daily&#x27;</span>,</span><br><span class="line">        <span class="string">&#x27;path&#x27;</span> =&gt; <span class="title function_ invoke__">storage_path</span>(<span class="string">&#x27;logs/exceptions.log&#x27;</span>),</span><br><span class="line">        <span class="string">&#x27;level&#x27;</span> =&gt; <span class="string">&#x27;error&#x27;</span>,</span><br><span class="line">    ],</span><br><span class="line">],</span><br></pre></td></tr></table></figure><p>在自定义异常处理器类的 <code>render</code> 方法中，可以使用日志记录器来记录异常信息：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">Log</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="title class_">Log</span>::<span class="title function_ invoke__">channel</span>(<span class="string">&#x27;exceptions&#x27;</span>)-&gt;<span class="title function_ invoke__">error</span>(<span class="variable">$exception</span>-&gt;<span class="title function_ invoke__">getMessage</span>());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，当发生异常时，异常信息将被记录到 <code>storage/logs/exceptions.log</code> 文件中。</p><h3 id="6-自定义异常响应：">6. 自定义异常响应：</h3><p>可以根据需要，为不同类型的异常定义自定义的响应格式。在自定义异常处理器类的 <code>render</code> 方法中，根据异常的类型返回不同的错误响应。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$exception</span> <span class="keyword">instanceof</span> CustomException) &#123;</span><br><span class="line">      <span class="comment">// 获取到自定义类的 data 数据</span></span><br><span class="line">      <span class="comment">// $data = $exception-&gt;data;</span></span><br><span class="line">      </span><br><span class="line">        <span class="keyword">return</span> <span class="title function_ invoke__">response</span>()-&gt;<span class="title function_ invoke__">json</span>([</span><br><span class="line">            <span class="string">&#x27;error&#x27;</span> =&gt; <span class="string">&#x27;Custom Error&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;message&#x27;</span> =&gt; <span class="variable">$exception</span>-&gt;<span class="title function_ invoke__">getMessage</span>(),</span><br><span class="line">        ], <span class="number">400</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="7-异常分类和处理：">7. 异常分类和处理：</h3><p>根据异常的类型或来源，将异常进行分类，并为每个分类定义相应的处理逻辑。在自定义异常处理器类的 <code>render</code> 方法中，根据异常的类型执行特定的处理操作。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$exception</span> <span class="keyword">instanceof</span> DatabaseException) &#123;</span><br><span class="line">        <span class="comment">// 处理数据库异常</span></span><br><span class="line">    &#125; <span class="keyword">elseif</span> (<span class="variable">$exception</span> <span class="keyword">instanceof</span> ApiException) &#123;</span><br><span class="line">        <span class="comment">// 处理 API 异常</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 默认处理逻辑</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="8-友好的错误页面：">8. 友好的错误页面：</h3><p>可以创建一个自定义的错误页面，用于显示异常信息。在自定义异常处理器类的 <code>render</code> 方法中，根据异常的类型或状态码返回相应的错误视图。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">isHttpException</span>(<span class="variable">$exception</span>)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="title function_ invoke__">response</span>()-&gt;<span class="title function_ invoke__">view</span>(<span class="string">&#x27;errors.custom&#x27;</span>, [], <span class="variable">$exception</span>-&gt;<span class="title function_ invoke__">getStatusCode</span>());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="9-异常监控和报警：">9. 异常监控和报警：</h3><p>可以使用 Laravel 提供的监控和报警工具，如 Laravel Telescope、Sentry 等，来监控和报警异常情况。</p><p>以下是一个使用 Sentry 的示例：</p><p>首先，安装 Sentry SDK：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">composer require sentry/sentry-laravel</span><br></pre></td></tr></table></figure><p>在 <code>.env</code> 文件中，配置 Sentry 的 DSN：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SENTRY_DSN=your-sentry-dsn</span><br></pre></td></tr></table></figure><p>在 <code>config/app.php</code> 文件中，将 <code>Sentry\Laravel\ServiceProvider::class</code> 添加到 <code>providers</code> 数组中。</p><p>然后，可以在自定义异常处理器类的 <code>render</code> 方法中使用 Sentry 来报告异常：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">Log</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Sentry</span>\<span class="title">State</span>\<span class="title">HubInterface</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">render</span>(<span class="params"><span class="variable">$request</span>, <span class="built_in">Exception</span> <span class="variable">$exception</span></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="title function_ invoke__">app</span>(<span class="title class_">HubInterface</span>::<span class="variable language_">class</span>)-&gt;<span class="title function_ invoke__">captureException</span>(<span class="variable">$exception</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">parent</span>::<span class="title function_ invoke__">render</span>(<span class="variable">$request</span>, <span class="variable">$exception</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，当发生异常时，Sentry 将捕获并报告异常信息。</p><h3 id="10-单元测试：">10. 单元测试：</h3><p>编写针对异常处理逻辑的单元测试，确保异常处理器的正确性和稳定性。可以使用 Laravel 提供的测试工具，如 PHPUnit，编写测试用例来覆盖不同类型的异常情况。</p><p>可以使用 Laravel 提供的 PHPUnit 测试框架编写单元测试用例来验证异常处理器的正确性和稳定性。以下是一个简单的示例：</p><p>创建一个测试类，例如 <code>ExceptionHandlingTest.php</code>，继承自 <code>TestCase</code>：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> <span class="title">Tests</span>\<span class="title">TestCase</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ExceptionHandlingTest</span> <span class="keyword">extends</span> <span class="title">TestCase</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testCustomException</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$response</span> = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">get</span>(<span class="string">&#x27;/custom-exception&#x27;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="variable">$response</span>-&gt;<span class="title function_ invoke__">assertStatus</span>(<span class="number">400</span>);</span><br><span class="line">        <span class="variable">$response</span>-&gt;<span class="title function_ invoke__">assertJson</span>([</span><br><span class="line">            <span class="string">&#x27;error&#x27;</span> =&gt; <span class="string">&#x27;Custom Error&#x27;</span>,</span><br><span class="line">        ]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在测试类中，编写测试方法来模拟触发自定义异常，并验证异常处理器的响应。</p><p>在 <code>routes/web.php</code> 文件中，定义一个路由来触发自定义异常：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Route</span>::<span class="title function_ invoke__">get</span>(<span class="string">&#x27;/custom-exception&#x27;</span>, function () &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">CustomException</span>(<span class="string">&#x27;Custom Error&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>运行单元测试：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">php artisan test</span><br></pre></td></tr></table></figure><p>以上是关于日志记录、异常监控和报警以及单元测试的简单示例。根据实际需求和使用的工具，可以进一步扩展和定制这些功能。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;
&lt;p&gt;全局异常处理是一种关键的开发实践，可以帮助我们更好地处理应用程序中的异常情况。本文将介绍如何在 Laravel 中实现全局异常处理，并探讨一些最佳实践，包括日志记录、异常监控和报警以及单元测试。&lt;/p&gt;
&lt;p&gt;在开发 Web 应用程序</summary>
      
    
    
    
    <category term="php" scheme="https://blog.mailjob.net/categories/php/"/>
    
    
    <category term="php" scheme="https://blog.mailjob.net/tags/php/"/>
    
  </entry>
  
  <entry>
    <title>Laravel数据查询优化最佳实践</title>
    <link href="https://blog.mailjob.net/posts/1844886816.html"/>
    <id>https://blog.mailjob.net/posts/1844886816.html</id>
    <published>2023-07-05T03:47:01.000Z</published>
    <updated>2026-07-25T09:01:29.902Z</updated>
    
    <content type="html"><![CDATA[<h2 id="简介">简介</h2><p>在 Laravel 中，数据库查询是一个常见的任务。为了提高查询的性能和可维护性，我们可以通过自定义查询构造器类来优化数据库查询。本文将详细解析使用 自定义ORM查询构造器类 <code>CacheBuilder</code> 和 改造 Laravel 中的 <code>DB</code> 类，以使用自定义的查询构造器类 <code>CacheBuilder</code>  缓存技巧来优化数据库查询。并详细解释每个方法的意义和改造的原因。</p><h2 id="优化方案">优化方案</h2><h3 id="ORM-优化">ORM 优化</h3><p>查询构造器类是执行数据库查询的重要组件之一。通过自定义查询构造器类，我们可以扩展和优化查询功能，以满足特定的需求。在本文中，我们将详细解析自定义查询构造器类 <code>CacheBuilder</code> 中的每个方法，以及为什么要进行这样的改造。</p><p>当使用 Laravel 框架的 ORM 建造者模式时，你可以自定义一个新的查询方法 <code>firstCache()</code> 来实现从缓存中读取数据的功能。下面是一个详细的代码改造过程示例：</p><ol><li>创建一个新的查询构造器类 <code>CacheBuilder</code>，继承自 Laravel 的原生查询构造器类 <code>Illuminate\Database\Query\Builder</code>。在这个类中，你可以添加自定义的方法 <code>firstCache()</code>。</li></ol><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Database</span>\<span class="title class_">Query</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Database</span>\<span class="title">Query</span>\<span class="title">Builder</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">Cache</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CacheBuilder</span> <span class="keyword">extends</span> <span class="title">Builder</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Execute the query and get the first result from the cache or the database.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span>  array|string  $columns</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> mixed</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">firstCache</span>(<span class="params"><span class="variable">$columns</span> = [<span class="string">&#x27;*&#x27;</span>]</span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="comment">// 1. 尝试从缓存中获取数据</span></span><br><span class="line">        <span class="variable">$cacheKey</span> = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">getCacheKey</span>();</span><br><span class="line">        <span class="variable">$cachedData</span> = <span class="title class_">Cache</span>::<span class="title function_ invoke__">get</span>(<span class="variable">$cacheKey</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$cachedData</span> !== <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 如果缓存中存在数据，则直接返回</span></span><br><span class="line">            <span class="keyword">return</span> <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">hydrateResults</span>(<span class="variable">$this</span>-&gt;model-&gt;<span class="title function_ invoke__">newCollection</span>([<span class="variable">$cachedData</span>]), <span class="variable">$columns</span>)-&gt;<span class="title function_ invoke__">first</span>();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 从数据库中获取数据</span></span><br><span class="line">        <span class="variable">$result</span> = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">first</span>(<span class="variable">$columns</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$result</span> !== <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">// 如果数据库中存在数据，则将其缓存起来</span></span><br><span class="line">            <span class="title class_">Cache</span>::<span class="title function_ invoke__">put</span>(<span class="variable">$cacheKey</span>, <span class="variable">$result</span>-&gt;<span class="title function_ invoke__">toArray</span>(), <span class="variable">$this</span>-&gt;<span class="title function_ invoke__">getCacheExpiration</span>());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$result</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Get the cache key for the query.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> string</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">getCacheKey</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="comment">// 这里可以根据你的需求生成一个唯一的缓存键</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27;cache_key_&#x27;</span> . <span class="title function_ invoke__">md5</span>(<span class="variable">$this</span>-&gt;<span class="title function_ invoke__">toSql</span>() . <span class="title function_ invoke__">serialize</span>(<span class="variable">$this</span>-&gt;<span class="title function_ invoke__">getBindings</span>()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Get the cache expiration time in seconds.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> int</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">getCacheExpiration</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="comment">// 这里可以根据你的需求设置缓存的过期时间</span></span><br><span class="line">        <span class="keyword">return</span> <span class="number">3600</span>; <span class="comment">// 1 hour</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码展示了 <code>CacheBuilder</code> 类的基本结构和示例的缓存优化方法。现在，让我们逐个解析每个方法的意义和改造原因：</p><ol><li><code>firstCache()</code> 方法<br><code>firstCache()</code> 方法是我们自定义的方法，用于缓存查询结果并返回第一个结果。在该方法中，我们首先尝试从缓存中获取数据，如果缓存中存在数据，则直接返回缓存结果，避免了对数据库的额外查询操作。如果缓存中不存在数据，则从数据库中获取数据，并将结果缓存起来，以便下次查询时可以直接从缓存中读取。通过这样的改造，我们可以减少对数据库的访问次数，提高查询性能。</li><li><code>getCacheKey()</code> 方法<br><code>getCacheKey()</code> 方法用于生成缓存键名。在该方法中，我们根据查询的 SQL 语句和绑定的参数生成一个唯一的缓存键。通过自定义键名的生成规则，我们可以更灵活地控制缓存的存储和管理。</li><li><code>getCacheExpiration()</code> 方法<br><code>getCacheExpiration()</code> 方法用于设置缓存的过期时间。在该方法中，我们可以根据需求设置缓存的有效期，以确保缓存的数据在一定时间后会被更新。通过设置适当的缓存过期时间，我们可以在一定程度上保持数据的实时性。</li></ol><p>通过以上的方法改造，我们实现了一个自定义的查询构造器类 <code>CacheBuilder</code>，它具备了缓存查询结果的能力。这样的改造使得我们可以更高效地执行数据库查询，减少对数据库的访问次数，从而提高查询性能和应用程序的响应速度。</p><p>接下来，让我们看一下如何在模型中使用自定义的查询构造器类 <code>CacheBuilder</code>。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Models</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">App</span>\<span class="title">Database</span>\<span class="title">Query</span>\<span class="title">CacheBuilder</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Database</span>\<span class="title">Eloquent</span>\<span class="title">Model</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">YourModel</span> <span class="keyword">extends</span> <span class="title">Model</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Get a new query builder instance for the connection.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span>  \Illuminate\Database\Query\Builder|null  $query</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> \Illuminate\Database\Query\Builder</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">newBaseQueryBuilder</span>(<span class="params"><span class="variable">$query</span> = <span class="literal">null</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$connection</span> = <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">getConnection</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 使用自定义的查询构造器类 CacheBuilder</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">CacheBuilder</span>(<span class="variable">$connection</span>, <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getQueryGrammar</span>(), <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getPostProcessor</span>(), <span class="variable">$query</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在你可以在使用模型查询时，使用新的方法 <code>firstCache()</code> 来从缓存中读取数据。例如：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$result</span> = <span class="title class_">YourModel</span>::<span class="title function_ invoke__">where</span>(<span class="string">&#x27;column&#x27;</span>, <span class="string">&#x27;value&#x27;</span>)-&gt;<span class="title function_ invoke__">firstCache</span>();</span><br></pre></td></tr></table></figure><p>这将先尝试从缓存中获取数据，如果缓存中不存在，则从数据库中读取数据，并将结果缓存起来。</p><p>请注意，以上代码示例仅为演示目的，你可能需要根据你的实际需求进行适当的修改和调整。另外，确保你已经正确配置了缓存驱动程序和相关的缓存设置。</p><h3 id="Query-Builder-优化">Query Builder 优化</h3><p>在 Laravel 中，我们通常使用 <code>DB</code> 类来执行数据库查询操作。然而，有时候我们需要对查询进行优化，以提高性能和可维护性。这时，自定义查询构造器类就派上了用场。</p><h3 id="继承ORM-Builder实现">继承ORM Builder实现</h3><p>在本文中，我们将介绍如何改造 Laravel 的 <code>DB</code> 类，以使用自定义的查询构造器类 <code>CacheBuilder</code>。这个改造将为我们提供一个名为 <code>firstCache()</code> 的方法，用于缓存查询结果，从而进一步提高查询性能。</p><p>首先，我们需要创建一个新的类 <code>CacheDB</code>，它继承自 Laravel 的原生 <code>DB</code> 类，并重写了 <code>connection</code> 方法。这个方法用于获取数据库连接实例。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Database</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">DB</span> <span class="keyword">as</span> <span class="title">BaseDB</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CacheDB</span> <span class="keyword">extends</span> <span class="title">BaseDB</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * Get a database connection instance.</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span>  string|null  $name</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> \Illuminate\Database\ConnectionInterface</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">static</span> <span class="function"><span class="keyword">function</span> <span class="title">connection</span>(<span class="params"><span class="variable">$name</span> = <span class="literal">null</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$connection</span> = <span class="built_in">parent</span>::<span class="title function_ invoke__">connection</span>(<span class="variable">$name</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 使用自定义的查询构造器类 CacheBuilder</span></span><br><span class="line">        <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">setQueryGrammar</span>(<span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getQueryGrammar</span>());</span><br><span class="line">        <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">setPostProcessor</span>(<span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getPostProcessor</span>());</span><br><span class="line">        <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">setQueryBuilder</span>(<span class="keyword">new</span> <span class="title class_">CacheBuilder</span>(<span class="variable">$connection</span>, <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getQueryGrammar</span>(), <span class="variable">$connection</span>-&gt;<span class="title function_ invoke__">getPostProcessor</span>()));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$connection</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码中的 <code>CacheDB</code> 类继承了原生的 <code>DB</code> 类，并重写了 <code>connection</code> 方法。这个方法在获取数据库连接实例时被调用。</p><p>在重写的 <code>connection</code> 方法中，我们首先调用了父类的 <code>connection</code> 方法，以获取原始的数据库连接实例。然后，我们使用自定义的查询构造器类 <code>CacheBuilder</code> 替换了原始连接实例的查询语法和后处理器，并设置了新的查询构造器。</p><p>现在，我们需要在 <code>config/app.php</code> 文件中进行一些配置更改，以使用我们的自定义 <code>CacheDB</code> 类。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;aliases&#x27;</span> =&gt; [</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="string">&#x27;DB&#x27;</span> =&gt; App\Database\CacheDB::<span class="variable language_">class</span>,</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">],</span><br></pre></td></tr></table></figure><p>在上述代码中，我们将原生的 <code>DB</code> 类别名替换为我们自定义的 <code>CacheDB</code> 类。这样，当我们使用 <code>DB</code> 类进行数据库查询时，实际上是使用了我们自定义的查询构造器类 <code>CacheBuilder</code>。</p><p>现在，我们可以使用 <code>firstCache()</code> 方法来缓存查询结果，以提高查询性能。例如：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$result</span> = DB::<span class="title function_ invoke__">table</span>(<span class="string">&#x27;your_table&#x27;</span>)-&gt;<span class="title function_ invoke__">where</span>(<span class="string">&#x27;column&#x27;</span>, <span class="string">&#x27;value&#x27;</span>)-&gt;<span class="title function_ invoke__">firstCache</span>();</span><br></pre></td></tr></table></figure><p>通过以上改造，我们实现了一个自定义的查询构造器类 <code>CacheBuilder</code>，并将其应用于 Laravel 的 <code>DB</code> 类。这样做的目的是为了提高数据库查询的性能和可维护性。</p><h3 id="独立写Cache">独立写Cache</h3><p>当使用 Laravel 框架时，你可以通过自定义一个扩展类来实现对 DB 建造者模式的改造，以满足你的需求。下面是一个示例代码：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title class_">App</span>\<span class="title class_">Extensions</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Database</span>\<span class="title">Query</span>\<span class="title">Builder</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Support</span>\<span class="title">Facades</span>\<span class="title">Cache</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CustomQueryBuilder</span> <span class="keyword">extends</span> <span class="title">Builder</span></span></span><br><span class="line"><span class="class"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">firstCache</span>(<span class="params"><span class="variable">$minutes</span>, <span class="variable">$key</span> = <span class="literal">null</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$key</span> = <span class="variable">$key</span> ?: <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">getCacheKey</span>();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="title class_">Cache</span>::<span class="title function_ invoke__">remember</span>(<span class="variable">$key</span>, <span class="variable">$minutes</span>, function () &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">first</span>();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">getCache</span>(<span class="params"><span class="variable">$minutes</span>, <span class="variable">$key</span> = <span class="literal">null</span></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="variable">$key</span> = <span class="variable">$key</span> ?: <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">getCacheKey</span>();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="title class_">Cache</span>::<span class="title function_ invoke__">remember</span>(<span class="variable">$key</span>, <span class="variable">$minutes</span>, function () &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="variable language_">$this</span>-&gt;<span class="title function_ invoke__">get</span>();</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">getCacheKey</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27;query_cache_&#x27;</span> . <span class="title function_ invoke__">sha1</span>(<span class="variable">$this</span>-&gt;<span class="title function_ invoke__">toSql</span>() . <span class="title function_ invoke__">serialize</span>(<span class="variable">$this</span>-&gt;<span class="title function_ invoke__">getBindings</span>()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述代码中，我们自定义了一个名为 <code>CustomQueryBuilder</code> 的类，它继承自 Laravel 框架的 <code>Builder</code> 类，也就是 DB 建造者类。这个自定义类添加了 <code>firstCache()</code> 和 <code>getCache()</code> 两个方法。</p><ul><li><code>firstCache()</code> 方法用于获取第一条记录并使用缓存。它接受两个参数：缓存的分钟数和可选的缓存键。如果缓存存在，将直接从缓存中获取数据；否则，将从数据库获取数据并生成缓存。最后，返回获取到的第一条记录。</li><li><code>getCache()</code> 方法用于获取多条记录并使用缓存。它接受两个参数：缓存的分钟数和可选的缓存键。与 <code>firstCache()</code> 方法类似，它也会先检查缓存是否存在，然后决定是直接从缓存中获取数据还是从数据库中获取数据并生成缓存。最后，返回获取到的多条记录。</li></ul><p>这两个方法内部使用了 Laravel 框架的 <code>Cache</code> 类来进行缓存操作。缓存键的生成使用了查询的 SQL 语句和绑定参数，以保证每个查询的唯一性。</p><p>为了使用这个自定义的 <code>CustomQueryBuilder</code> 类，你需要在 Laravel 项目中注册这个自定义类作为 DB 建造者的默认类。可以在 <code>AppServiceProvider</code> 或其他合适的服务提供者中添加以下代码：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Database</span>\<span class="title">ConnectionInterface</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">App</span>\<span class="title">Extensions</span>\<span class="title">CustomQueryBuilder</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">register</span>(<span class="params"></span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="variable language_">$this</span>-&gt;app-&gt;<span class="title function_ invoke__">bind</span>(<span class="title class_">ConnectionInterface</span>::<span class="variable language_">class</span>, function (<span class="variable">$app</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">CustomQueryBuilder</span>(</span><br><span class="line">            <span class="variable">$app</span>[<span class="string">&#x27;db&#x27;</span>]-&gt;<span class="title function_ invoke__">connection</span>(),</span><br><span class="line">            <span class="variable">$app</span>[<span class="string">&#x27;db&#x27;</span>]-&gt;<span class="title function_ invoke__">getQueryGrammar</span>(),</span><br><span class="line">            <span class="variable">$app</span>[<span class="string">&#x27;db&#x27;</span>]-&gt;<span class="title function_ invoke__">getPostProcessor</span>()</span><br><span class="line">        );</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码将绑定 <code>ConnectionInterface</code> 接口到 <code>CustomQueryBuilder</code> 类，以便在使用 <code>DB</code> 门面时默认使用你的自定义类。</p><p>这样，当你使用 <code>DB</code> 门面的 <code>table()</code> 方法时，将使用你自定义的 <code>CustomQueryBuilder</code> 类，从而具备了 <code>firstCache()</code> 和 <code>getCache()</code> 方法的功能。</p><p><strong>使用封装的方法</strong></p><p>现在你可以在项目中使用 <code>firstCache()</code> 和 <code>getCache()</code> 方法来获取缓存数据。以下是示例用法：</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$users</span> = DB::<span class="title function_ invoke__">table</span>(<span class="string">&#x27;users&#x27;</span>)-&gt;<span class="title function_ invoke__">firstCache</span>(<span class="number">60</span>); <span class="comment">// 获取第一条用户数据并使用缓存（缓存有效期为60分钟）</span></span><br></pre></td></tr></table></figure><p>在上述示例中，我们使用 <code>DB::table('users')</code> 获取了一个查询构建器，然后通过调用 <code>firstCache(60)</code> 方法来获取第一条用户数据。如果缓存存在，将直接从缓存中获取数据，否则将从数据库中查询并生成缓存。</p><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$users</span> = DB::<span class="title function_ invoke__">table</span>(<span class="string">&#x27;users&#x27;</span>)-&gt;<span class="title function_ invoke__">getCache</span>(<span class="number">60</span>); <span class="comment">// 获取所有用户数据并使用缓存（缓存有效期为60分钟）</span></span><br></pre></td></tr></table></figure><p>这个示例展示了如何使用 <code>getCache(60)</code> 方法来获取所有的用户数据，并使用缓存。同样地，如果缓存存在，数据将直接从缓存中获取，否则将从数据库中查询并生成缓存。</p><h2 id="总结：">总结：</h2><p>通过自定义查询构造器类的改造，我们可以优化 Laravel 的数据库查询。在本文中，我们介绍了如何改造 <code>DB</code> 类，使用自定义的查询构造器类 <code>CacheBuilder</code>。这个改造允许我们使用新的方法 <code>firstCache()</code> 来缓存查询结果，从而提高查询性能。通过重写 <code>connection</code> 方法，我们成功地将自定义的查询构造器类应用于 <code>DB</code> 类，并在配置文件中进行了相应的修改。这样的改造提供了更高效和可扩展的数据库查询功能，使得我们能够更好地优化和管理我们的应用程序。无论是对于初学者还是有经验的开发者来说，这种改造都是非常有益的。</p><h2 id="Laravel其他优化方法">Laravel其他优化方法</h2><p>除了对 Laravel 的建造者模式进行缓存优化外，还有许多其他的优化方式可以提升 Laravel 框架的性能和效率。以下是一些常见的优化方式：</p><ol><li>缓存配置和路由：<ul><li>使用缓存来存储配置文件，以减少每次请求时重新加载配置的开销。</li><li>缓存路由信息，避免在每次请求时重新解析路由。</li></ul></li><li>使用缓存：<ul><li>使用缓存来存储经常访问的数据，如数据库查询结果、API 响应等。</li><li>使用适当的缓存驱动（如 Memcached、Redis）来提高缓存性能。</li></ul></li><li>数据库优化：<ul><li>使用适当的索引来加速数据库查询。</li><li>避免在循环中执行数据库查询，尽量使用批量操作。</li><li>使用延迟加载（Lazy Loading）来减少关联模型的查询次数。</li></ul></li><li>代码优化：<ul><li>避免在视图中执行复杂的逻辑操作，尽量将逻辑放在控制器或服务层中处理。</li><li>使用 Eager Loading 来预加载关联模型，减少查询次数。</li><li>使用合适的数据结构和算法来提高代码的效率。</li></ul></li><li>使用队列：<ul><li>将耗时的任务放入队列中异步处理，提高应用程序的响应速度。</li><li>使用适当的队列驱动（如 Redis、Beanstalkd）来提高队列的处理性能。</li></ul></li><li>优化自动加载：<ul><li>使用 Composer 的 <code>classmap</code> 自动加载优化，将类映射到文件路径，减少自动加载的开销。</li><li>避免加载不必要的类和文件。</li></ul></li><li>使用缓存视图：<ul><li>将编译后的视图缓存起来，减少视图编译的开销。</li></ul></li><li>使用 HTTP 缓存：<ul><li>设置适当的缓存头，利用浏览器缓存和 CDN 缓存来减少重复请求。</li></ul></li><li>使用性能分析工具：<ul><li>使用工具如 Laravel Debugbar、Blackfire 等来分析应用程序的性能瓶颈，并进行相应的优化。</li></ul></li></ol><p>这些优化方式可以根据具体的应用场景和需求进行选择和实施。通过综合应用这些优化策略，可以显著提升 Laravel 应用程序的性能和效率。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;简介&quot;&gt;简介&lt;/h2&gt;
&lt;p&gt;在 Laravel 中，数据库查询是一个常见的任务。为了提高查询的性能和可维护性，我们可以通过自定义查询构造器类来优化数据库查询。本文将详细解析使用 自定义ORM查询构造器类 &lt;code&gt;CacheBuilder&lt;/code&gt; 和 改</summary>
      
    
    
    
    <category term="php" scheme="https://blog.mailjob.net/categories/php/"/>
    
    
    <category term="php" scheme="https://blog.mailjob.net/tags/php/"/>
    
  </entry>
  
  <entry>
    <title>shell检查php项目是否存在语法错误</title>
    <link href="https://blog.mailjob.net/posts/3069224484.html"/>
    <id>https://blog.mailjob.net/posts/3069224484.html</id>
    <published>2023-06-29T06:27:25.000Z</published>
    <updated>2026-07-25T09:01:29.902Z</updated>
    
    <content type="html"><![CDATA[<p>当检查一个PHP项目中是否存在语法错误时，我们可以使用Shell脚本来自动化这个过程。在本文中，我们将介绍两种方法来实现这个目标。</p><h2 id="方法一：使用串行方式">方法一：使用串行方式</h2><p>首先，我们可以使用一个简单的Shell脚本来遍历项目目录中的所有PHP文件，并使用<code>php -l</code>命令来检查每个文件是否存在语法错误。以下是实现这个方法的脚本：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置项目路径</span></span><br><span class="line">project_path=&quot;/path/to/your/php/project&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">遍历项目目录中的所有php文件</span></span><br><span class="line">for file in $(find $project_path -type f -name &quot;*.php&quot;); do</span><br><span class="line">    # 检查文件是否存在语法错误</span><br><span class="line">    php -l $file</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p>这个脚本将会遍历项目目录中的所有PHP文件，并使用<code>php -l</code>命令来检查每个文件是否存在语法错误。如果存在语法错误，将会输出错误信息。</p><h2 id="方法二：使用并行方式">方法二：使用并行方式</h2><p>如果你想要加快检查的速度，你可以使用以下方式同时开启多个线程并行检查PHP文件的语法错误。以下是实现这个方法的脚本：</p><h3 id="xargs命令">xargs命令</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置项目路径</span></span><br><span class="line">project_path=&quot;/path/to/your/php/project&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">定义函数，用于检查语法错误</span></span><br><span class="line">check_syntax() &#123;</span><br><span class="line">    php -l &quot;$1&quot;</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">导出函数，以便在xargs中使用</span></span><br><span class="line">export -f check_syntax</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">遍历项目目录中的所有php文件，并使用xargs并行执行检查语法错误的函数</span></span><br><span class="line">find &quot;$project_path&quot; -type f -name &quot;*.php&quot; | xargs -I &#123;&#125; -P 20 bash -c &#x27;check_syntax &quot;$@&quot;&#x27; _ &#123;&#125;</span><br></pre></td></tr></table></figure><p>推荐下面这种写法，如果遇到语法错误，直接中断脚本执行。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">提示用户输入项目路径</span></span><br><span class="line">project_path=&quot;$1&quot;</span><br><span class="line"></span><br><span class="line">echo &quot;项目路径: $project_path&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">错误处理</span></span><br><span class="line">handle_error() &#123;</span><br><span class="line">    echo &quot;发生了错误，请检查脚本或项目路径。&quot;</span><br><span class="line">    exit 1</span><br><span class="line">&#125;</span><br><span class="line">trap &#x27;handle_error&#x27; ERR</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">遍历项目目录中的所有php文件，并使用xargs并行执行检查语法错误的命令</span></span><br><span class="line">find &quot;$project_path&quot; -type f -name &quot;*.php&quot; -print0 | xargs -0 -I &#123;&#125; -P 20 bash -c &#x27;php -l &quot;&#123;&#125;&quot;&#x27; &amp;&amp; echo &quot;OK&quot;</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">排除 vendor 目录</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">find <span class="string">&quot;<span class="variable">$project_path</span>&quot;</span> -<span class="built_in">type</span> f -name <span class="string">&quot;*.php&quot;</span> -not -path <span class="string">&#x27;*/vendor/*&#x27;</span> -print0 | xargs -0 -I &#123;&#125; -P 20 bash -c <span class="string">&#x27;php -l &quot;&#123;&#125;&quot;&#x27;</span> &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;OK&quot;</span></span></span><br></pre></td></tr></table></figure><p><strong>工作原理</strong></p><ol><li>设置项目路径变量：将<code>/path/to/your/php/project</code>替换为你的PHP项目的实际路径。</li><li>定义检查语法错误的函数：<code>check_syntax</code>函数使用<code>php -l</code>命令来检查给定的PHP文件是否存在语法错误。</li><li>导出函数：使用<code>export -f</code>命令导出<code>check_syntax</code>函数，以便在<code>xargs</code>中使用。</li><li>遍历项目目录中的所有PHP文件：使用<code>find</code>命令查找项目目录中的所有PHP文件，并将它们传递给<code>xargs</code>命令。</li><li>并行执行检查语法错误的函数：<code>xargs -I {} -P 20</code>命令将每个PHP文件作为参数传递给<code>bash -c 'check_syntax &quot;$@&quot;' _ {}</code>命令。<code>-P 20</code>表示最多开启20个线程并行执行检查语法错误的函数。</li><li>检查语法错误：每个线程将使用<code>check_syntax</code>函数检查一个PHP文件是否存在语法错误。如果存在语法错误，将会输出错误信息。</li></ol><h3 id="parallel命令">parallel命令</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置项目路径</span></span><br><span class="line">project_path=&quot;/path/to/your/php/project&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">使用GNU Parallel工具并行检查语法错误</span></span><br><span class="line">find $project_path -type f -name &quot;*.php&quot; | parallel -j 20 php -l &#123;&#125;</span><br></pre></td></tr></table></figure><ol><li>设置项目路径变量：将<code>/path/to/your/php/project</code>替换为你的PHP项目的实际路径。</li><li>使用GNU Parallel工具并行检查语法错误：<code>find $project_path -type f -name &quot;*.php&quot;</code>命令用于查找项目目录中的所有PHP文件，并将它们作为输入传递给<code>parallel</code>命令。</li><li><code>-j 20</code>参数表示最多开启20个线程并行执行后续的命令。</li><li><code>php -l {}</code>命令用于检查每个PHP文件是否存在语法错误。<code>{}</code>是一个占位符，会被当前正在处理的文件名替换。</li></ol><h2 id="总结">总结</h2><p>使用Shell脚本来检查PHP项目中是否存在语法错误是一种简单而有效的方法。你可以选择使用串行方式或并行方式来实现这个目标，具体取决于你的需求和项目的规模。无论你选择哪种方法，都能够帮助你及时发现并修复PHP项目中的语法错误，提高代码的质量和可靠性。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;当检查一个PHP项目中是否存在语法错误时，我们可以使用Shell脚本来自动化这个过程。在本文中，我们将介绍两种方法来实现这个目标。&lt;/p&gt;
&lt;h2 id=&quot;方法一：使用串行方式&quot;&gt;方法一：使用串行方式&lt;/h2&gt;
&lt;p&gt;首先，我们可以使用一个简单的Shell脚本来遍历项目目录</summary>
      
    
    
    
    <category term="php" scheme="https://blog.mailjob.net/categories/php/"/>
    
    
    <category term="php" scheme="https://blog.mailjob.net/tags/php/"/>
    
  </entry>
  
  <entry>
    <title>千万级记录数据表结构如何实现平滑变更</title>
    <link href="https://blog.mailjob.net/posts/2674700618.html"/>
    <id>https://blog.mailjob.net/posts/2674700618.html</id>
    <published>2023-06-28T05:53:25.000Z</published>
    <updated>2026-07-25T09:01:29.900Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题描述">问题描述</h2><p>数据量大、并发量高场景，<strong>如何在流量低峰期，平滑实施表结构变更？</strong></p><p>一般来说，是指增加表的属性，因为：</p><ul><li>如果是减column，升级程序不使用即可；</li><li>如果是修改column，程序兼容性容易出问题；</li></ul><h2 id="常见方案">常见方案</h2><p>方案一：在线修改表结构。</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">alter table</span> <span class="keyword">add</span> <span class="keyword">column</span></span><br></pre></td></tr></table></figure><p>数据量大的情况下，锁表时间会较长，造成拒绝服务，一般不可行。</p><p><strong>方案二：通过增加表的方式扩展属性，通过外键join来查询。</strong></p><p>举个例子，对：<br><em>t_user(uid, c1, c2, c3)</em><br>想要扩展属性，可以通过增加一个表实现：<br><em>t_user_ex(uid, c4, c5, c6)</em></p><p>数据量大的情况下，join性能较差，一般不可行。</p><p><strong>方案三，通过增加表的方式扩展，通过视图来屏蔽底层复杂性。</strong></p><p>同上，视图效率较低，一般不使用视图。</p><p><strong>方案四，提前预留一些reserved字段，加列可复用这些字段。</strong></p><p>这个方案可行，但如果预留过多，会造成空间浪费。</p><p><strong>方案六，pt-online-schema-change</strong></p><p>对于MySQL而言，这是目前比较成熟的方案，被广大公司所使用。</p><h2 id="pt-online-schema-change方案说明">pt-online-schema-change方案说明</h2><p>假设：</p><p><em>user(uid, name, passwd)</em></p><p>要扩展到：</p><p><em>user(uid, name, passwd, age, sex)</em></p><p><strong>第一步</strong>，先创建一个扩充字段后的新表：</p><p><em>user_new(uid, name, passwd, age, sex)</em></p><p><strong>第二步</strong>，在原表user上创建三个触发器，对原表user进行的所有insert/delete/update操作，都会对新表user_new进行相同的操作；</p><p><strong>第三步</strong>，分批将原表user中的数据insert到新表user_new，直至数据迁移完成；</p><p><strong>第四步</strong>，删掉触发器，把原表移走（默认是drop掉）；</p><p><strong>第五步</strong>，把新表user_new重命名（rename）成原表user；<br>扩充字段完成，整个过程不需要锁表，可以持续对外提供服务。</p><p><strong>操作过程中需要注意：</strong></p><ol><li>变更过程中，最重要的是冲突的处理，一条原则，以触发器的新数据为准，这就要求被迁移的表<strong>必须有主键</strong>（这个要求基本都满足）；</li><li>变更过程中，写操作需要建立触发器，所以如果原表已经有很多触发器，方案就不行（互联网大数据高并发的在线业务，一般都禁止使用触发器）；</li><li>触发器的建立，会影响原表的性能，所以这个操作必须在流量低峰期进行；</li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;问题描述&quot;&gt;问题描述&lt;/h2&gt;
&lt;p&gt;数据量大、并发量高场景，&lt;strong&gt;如何在流量低峰期，平滑实施表结构变更？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般来说，是指增加表的属性，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果是减column，升级程序不使用即可；&lt;/li&gt;</summary>
      
    
    
    
    <category term="mysql" scheme="https://blog.mailjob.net/categories/mysql/"/>
    
    
    <category term="mysql" scheme="https://blog.mailjob.net/tags/mysql/"/>
    
  </entry>
  
  <entry>
    <title>如何用go实现一个ORM</title>
    <link href="https://blog.mailjob.net/posts/1696354643.html"/>
    <id>https://blog.mailjob.net/posts/1696354643.html</id>
    <published>2023-02-22T06:32:00.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<p>为了提高开发效率和质量，我们常常需要ORM来帮助我们快速实现持久层增删改查API，目前go语言实现的ORM有很多种，他们都有自己的优劣点，有的实现简单，有的功能复杂，有的API十分优雅。在使用了多个类似的工具之后，总是会发现某些点无法满足解决我们生产环境中碰到的实际问题，比如无法集成公司内部的监控，Trace组件，没有database层的超时设置，没有熔断等，所以有必要公司自己内部实现一款满足我们可自定义开发的ORM，好用的生产工具常常能够对生产力产生飞跃式的提升。</p><h2 id="为什么需要ORM">为什么需要ORM</h2><h3 id="直接使用database-sql的痛点">直接使用database/sql的痛点</h3><p>首先看看用database/sql如何查询数据库<br>我们用user表来做例子，一般的工作流程是先做技术方案，其中排在比较前面的是数据库表的设计，大部分公司应该有严格的数据库权限控制，不会给线上程序使用比较危险的操作权限，比如创建删除数据库，表，删除数据等。<br>表结构如下：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> `<span class="keyword">user</span>` (</span><br><span class="line">  `id` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `name` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;名称&#x27;</span>,</span><br><span class="line">  `age` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;年龄&#x27;</span>,</span><br><span class="line">  `ctime` datetime <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `mtime` datetime <span class="keyword">NOT NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;更新时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB  <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4</span><br></pre></td></tr></table></figure><p>首先我们要写出和表结构对应的结构体User，如果你足够勤奋和努力，相应的json tag 和注释都可以写上，这个过程无聊且重复，因为在设计表结构的时候你已经写过一遍了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Id    <span class="type">int64</span>     <span class="string">`json:&quot;id&quot;`</span>   </span><br><span class="line">    Name  <span class="type">string</span>    <span class="string">`json:&quot;name&quot;`</span></span><br><span class="line">    Age   <span class="type">int64</span>    </span><br><span class="line">    Ctime time.Time</span><br><span class="line">    Mtime time.Time  <span class="comment">// 更新时间</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>定义好结构体，我们写一个查询年龄在20以下且按照id字段顺序排序的前20名用户的 go代码</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">FindUsers</span><span class="params">(ctx context.Context)</span></span> ([]*User, <span class="type">error</span>) &#123;</span><br><span class="line">    rows, err := db.QueryContext(ctx, <span class="string">&quot;SELECT `id`,`name`,`age`,`ctime`,`mtime` FROM user WHERE `age`&lt;? ORDER BY `id` LIMIT 20 &quot;</span>, <span class="number">20</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> rows.Close()</span><br><span class="line">    result := []*User&#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> rows.Next() &#123;</span><br><span class="line">        a := &amp;User&#123;&#125;</span><br><span class="line">        <span class="keyword">if</span> err := rows.Scan(&amp;a.Id, &amp;a.Name, &amp;a.Age, &amp;a.Ctime, &amp;a.Mtime); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">        &#125;</span><br><span class="line">        result = <span class="built_in">append</span>(result, a)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> rows.Err() != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, rows.Err()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我们写少量这样的代码的时候我们可能还觉得轻松，但是当你业务工期排的很紧，并且要写大量的定制化查询的时候，这样的重复代码会越来越多。<br>上面的的代码我们发现有这么几个问题：</p><ol><li>SQL 语句是硬编码在程序里面的，当我需要增加查询条件的时候我需要另外再写一个方法，整个方法需要拷贝一份，很不灵活。</li><li>在查询表所有字段的情况下，第2行下面的代码都是一样重复的，不管sql语句后面的条件是怎么样的。</li><li>我们发现第1行SQL语句编写和rows.Scan()那行，写的枯燥层度是和表字段的数量成正比的，如果一个表有50个字段或者100个字段，手写是非常乏味的。</li><li>在开发过程中rows.Close() 和 rows.Err()忘记写是常见的错误。</li></ol><p>我们总结出来用database/sql标准库开发的痛点：</p><h4 id="开发效率很低">开发效率很低</h4><p>很显然写上面的那种代码是很耗费时间的，因为手误容易写错，无可避免要增加自测的时间。如果上面的结构体User、 查询方法FindUsers() 代码能够自动生成，那么那将会极大的提高开发效率并且减少human error的发生从而提高开发质量。</p><h4 id="心智负担很重">心智负担很重</h4><p>如果一个开发人员把大量的时间花在这些代码上，那么他其实是在浪费自己的时间，不管在工作中还是在个人项目中，应该把重点花在架构设计，业务逻辑设计，困难点攻坚上面，去探索和开拓自己没有经验的领域，这块Dao层的代码最好在10分钟内完成。</p><h2 id="ORM的核心组成">ORM的核心组成</h2><p>明白了上面的痛点，为了开发工作更舒服，更高效，我们尝试着自己去开发一个ORM，核心的地方在于两个方面：</p><p><img src="http://img.github.mailjob.net/202302221448782.png" alt="图片"></p><ol><li><p>SQLBuilder：SQL语句要非硬编码，通过某种链式调用构造器帮助我构建SQL语句。</p></li><li><p>Scanner：从数据库返回的数据可以自动映射赋值到结构体中。</p></li></ol><h3 id="SQL-SelectBuilder">SQL SelectBuilder</h3><p>我们尝试做个简略版的查询语句构造器,最终我们要达到如下图所示的效果。</p><p><img src="http://img.github.mailjob.net/202302221448335.png" alt="图片"></p><p>我们可以通过和SQL关键字同名的方法来表达SQL语句的固有关键字，通过go方法参数来设置其中动态变化的元素，这样链式调用和写SQL语句的思维顺序是一致的，只不过我们之前通过硬编码的方式变成了方法调用。</p><p>具体代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">type</span> SelectBuilder <span class="keyword">struct</span> &#123;</span><br><span class="line">    builder   *strings.Builder</span><br><span class="line">    column    []<span class="type">string</span></span><br><span class="line">    tableName <span class="type">string</span></span><br><span class="line">    where     []<span class="function"><span class="keyword">func</span><span class="params">(s *SelectBuilder)</span></span></span><br><span class="line">    args      []<span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">    orderby   <span class="type">string</span></span><br><span class="line">    offset    *<span class="type">int64</span></span><br><span class="line">    limit     *<span class="type">int64</span></span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> Select(field ...<span class="type">string</span>) *SelectBuilder &#123;</span><br><span class="line">    s.column = <span class="built_in">append</span>(s.column, field...)</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> From(name <span class="type">string</span>) *SelectBuilder &#123;</span><br><span class="line">    s.tabelName = name</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> Where(f ...<span class="keyword">func</span>(s *SelectBuilder)) *SelectBuilder &#123;</span><br><span class="line">    s.where = <span class="built_in">append</span>(s.where, f...)</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> OrderBy(field <span class="type">string</span>) *SelectBuilder &#123;</span><br><span class="line">    s.orderby = field</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> Limit(offset, limit <span class="type">int64</span>) *SelectBuilder &#123;</span><br><span class="line">    s.offset = &amp;offset</span><br><span class="line">    s.limit = &amp;limit</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GT</span><span class="params">(field <span class="type">string</span>, arg <span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="function"><span class="keyword">func</span><span class="params">(s *SelectBuilder)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *SelectBuilder)</span></span> &#123;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot;`&quot;</span> + field + <span class="string">&quot;`&quot;</span> + <span class="string">&quot; &gt; ?&quot;</span>)</span><br><span class="line">        s.args = <span class="built_in">append</span>(s.args, arg)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *SelectBuilder)</span></span> Query() (<span class="type">string</span>, []<span class="keyword">interface</span>&#123;&#125;) &#123;</span><br><span class="line">    s.builder.WriteString(<span class="string">&quot;SELECT &quot;</span>)</span><br><span class="line">    <span class="keyword">for</span> k, v := <span class="keyword">range</span> s.column &#123;</span><br><span class="line">        <span class="keyword">if</span> k &gt; <span class="number">0</span> &#123;</span><br><span class="line">            s.builder.WriteString(<span class="string">&quot;,&quot;</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot;`&quot;</span> + v + <span class="string">&quot;`&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    s.builder.WriteString(<span class="string">&quot; FROM &quot;</span>)</span><br><span class="line">    s.builder.WriteString(<span class="string">&quot;`&quot;</span> + s.tableName + <span class="string">&quot;` &quot;</span>)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(s.where) &gt; <span class="number">0</span> &#123;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot;WHERE &quot;</span>)</span><br><span class="line">        <span class="keyword">for</span> k, f := <span class="keyword">range</span> s.where &#123;</span><br><span class="line">            <span class="keyword">if</span> k &gt; <span class="number">0</span> &#123;</span><br><span class="line">                s.builder.WriteString(<span class="string">&quot; AND &quot;</span>)</span><br><span class="line">            &#125;</span><br><span class="line">            f(s)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> s.orderby != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot; ORDER BY &quot;</span> + s.orderby)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> s.limit != <span class="literal">nil</span> &#123;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot; LIMIT &quot;</span>)</span><br><span class="line">        s.builder.WriteString(strconv.FormatInt(*s.limit, <span class="number">10</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> s.offset != <span class="literal">nil</span> &#123;</span><br><span class="line">        s.builder.WriteString(<span class="string">&quot; OFFSET &quot;</span>)</span><br><span class="line">        s.builder.WriteString(strconv.FormatInt(*s.offset, <span class="number">10</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> s.builder.String(), s.args</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>通过结构体上的方法调用返回自身，使其具有链式调用能力，并通过方法调用设置结构体中的值，用以构成SQL语句需要的元素。</li><li>SelectBuilder 包含性能较高的strings.Builder 来拼接字符串。</li><li>Query()方法构建出真正的SQL语句，返回包含占位符的SQL语句和args参数。</li><li>[]func(s *SelectBuilder)通过函数数组来创建查询条件，可以通过函数调用的顺序和层级来生成 AND OR这种有嵌套关系的查询条件子句。</li><li>Where() 传入的是查询条件函数，为可变参数列表，查询条件之间默认是AND关系。</li></ol><p>外部使用起来效果：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">b := SelectBuilder&#123;builder: &amp;strings.Builder&#123;&#125;&#125;</span><br><span class="line">sql, args := b.</span><br><span class="line">    Select(<span class="string">&quot;id&quot;</span>, <span class="string">&quot;name&quot;</span>, <span class="string">&quot;age&quot;</span>, <span class="string">&quot;ctime&quot;</span>, <span class="string">&quot;mtime&quot;</span>).</span><br><span class="line">    From(<span class="string">&quot;user&quot;</span>).</span><br><span class="line">    Where(GT(<span class="string">&quot;id&quot;</span>, <span class="number">0</span>), GT(<span class="string">&quot;age&quot;</span>, <span class="number">0</span>)).</span><br><span class="line">    OrderBy(<span class="string">&quot;id&quot;</span>).</span><br><span class="line">    Limit(<span class="number">0</span>, <span class="number">20</span>).</span><br><span class="line">    Query()</span><br></pre></td></tr></table></figure><h3 id="Scanner的实现">Scanner的实现</h3><p>顾名思义Scanner的作用就是把查询结果设置到对应的go对象上去，完成关系和对象的映射，关键核心就是通过反射获知传入对象的类型和字段类型，通过反射创建对象和值，并通过golang结构体的字段后面的tag来和查询结果的表头一一对应，达到动态给结构字段赋值的能力。</p><p><img src="http://img.github.mailjob.net/202302221448220.png" alt="图片"></p><p>具体实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ScanSlice</span><span class="params">(rows *sql.Rows, dst <span class="keyword">interface</span>&#123;&#125;)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> rows.Close()</span><br><span class="line">    <span class="comment">// dst的地址</span></span><br><span class="line">    val := reflect.ValueOf(dst) <span class="comment">//  &amp;[]*main.User</span></span><br><span class="line">    <span class="comment">// 判断是否是指针类型，go是值传递，只有传指针才能让更改生效</span></span><br><span class="line">    <span class="keyword">if</span> val.Kind() != reflect.Ptr &#123;</span><br><span class="line">        <span class="keyword">return</span> errors.New(<span class="string">&quot;dst not a pointer&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 指针指向的Value</span></span><br><span class="line">    val = reflect.Indirect(val) <span class="comment">// []*main.User</span></span><br><span class="line">    <span class="keyword">if</span> val.Kind() != reflect.Slice &#123;</span><br><span class="line">        <span class="keyword">return</span> errors.New(<span class="string">&quot;dst not a pointer to slice&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 获取slice中的类型</span></span><br><span class="line">    struPointer := val.Type().Elem() <span class="comment">// *main.User</span></span><br><span class="line">    <span class="comment">// 指针指向的类型 具体结构体</span></span><br><span class="line">    stru := struPointer.Elem()      <span class="comment">//  main.User</span></span><br><span class="line"> </span><br><span class="line">    cols, err := rows.Columns()  <span class="comment">// [id,name,age,ctime,mtime]</span></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 判断查询的字段数是否大于 结构体的字段数</span></span><br><span class="line">    <span class="keyword">if</span> stru.NumField() &lt; <span class="built_in">len</span>(cols) &#123; <span class="comment">// 5,5</span></span><br><span class="line">        <span class="keyword">return</span> errors.New(<span class="string">&quot;NumField and cols not match&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//结构体的json tag的value对应字段在结构体中的index</span></span><br><span class="line">    tagIdx := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="type">string</span>]<span class="type">int</span>) <span class="comment">//map tag -&gt; field idx</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; stru.NumField(); i++ &#123;</span><br><span class="line">        tagname := stru.Field(i).Tag.Get(<span class="string">&quot;json&quot;</span>)</span><br><span class="line">        <span class="keyword">if</span> tagname != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">            tagIdx[tagname] = i</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    resultType := <span class="built_in">make</span>([]reflect.Type, <span class="number">0</span>, <span class="built_in">len</span>(cols)) <span class="comment">// [int64,string,int64,time.Time,time.Time]</span></span><br><span class="line">    index := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">0</span>, <span class="built_in">len</span>(cols))               <span class="comment">// [0,1,2,3,4,5]</span></span><br><span class="line">    <span class="comment">// 查找和列名相对应的结构体jsontag name的字段类型，保存类型和序号到resultType和index中</span></span><br><span class="line">    <span class="keyword">for</span> _, v := <span class="keyword">range</span> cols &#123;</span><br><span class="line">        <span class="keyword">if</span> i, ok := tagIdx[v]; ok &#123;</span><br><span class="line">            resultType = <span class="built_in">append</span>(resultType, stru.Field(i).Type)</span><br><span class="line">            index = <span class="built_in">append</span>(index, i)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> rows.Next() &#123;</span><br><span class="line">        <span class="comment">// 创建结构体指针,获取指针指向的对象</span></span><br><span class="line">        obj := reflect.New(stru).Elem()                   <span class="comment">// main.User</span></span><br><span class="line">        result := <span class="built_in">make</span>([]<span class="keyword">interface</span>&#123;&#125;, <span class="number">0</span>, <span class="built_in">len</span>(resultType)) <span class="comment">//[]</span></span><br><span class="line">        <span class="comment">// 创建结构体字段类型实例的指针,并转化为interface&#123;&#125; 类型</span></span><br><span class="line">        <span class="keyword">for</span> _, v := <span class="keyword">range</span> resultType &#123;</span><br><span class="line">            result = <span class="built_in">append</span>(result, reflect.New(v).Interface()) <span class="comment">// *Int64 ,*string ....</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 扫描结果</span></span><br><span class="line">        err := rows.Scan(result...)</span><br><span class="line">        <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> err</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> i, v := <span class="keyword">range</span> result &#123;</span><br><span class="line">            <span class="comment">// 找到对应的结构体index</span></span><br><span class="line">            fieldIndex := index[i]</span><br><span class="line">            <span class="comment">// 把scan 后的值通过反射得到指针指向的value，赋值给对应的结构体字段</span></span><br><span class="line">            obj.Field(fieldIndex).Set(reflect.ValueOf(v).Elem()) <span class="comment">// 给obj 的每个字段赋值</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// append 到slice</span></span><br><span class="line">        vv := reflect.Append(val, obj.Addr()) <span class="comment">// append到 []*main.User, maybe addr change</span></span><br><span class="line">        val.Set(vv)                           <span class="comment">// []*main.User</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> rows.Err()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过反射赋值流程，如果想知道具体的实现细节可以仔细阅读上面代码里面的注释</p><p><img src="http://img.github.mailjob.net/202302221448957.png" alt="图片"></p><ol><li>以上主要的思想就是通过reflect包来获取传入dst的Slice类型，并通过反射创建其包含的对象，具体的步骤和解释请仔细阅读注释和图例。</li><li>通过指定的json tag 可以把查询结果和结构体字段mapping起来，即使查询语句中字段不按照表结构顺序。</li><li>ScanSlice是通用的Scanner。</li><li>使用反射创建对象明显创建了多余的对象，没有传统的方式赋值高效，但是换来的巨大的灵活性在某些场景下是值得的。</li></ol><p>有了SQLBuilder和Scanner 我们就可以这样写查询函数了：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">FindUserReflect</span><span class="params">()</span></span> ([]*User, <span class="type">error</span>) &#123;</span><br><span class="line">    b := SelectBuilder&#123;builder: &amp;strings.Builder&#123;&#125;&#125;</span><br><span class="line">    sql, args := b.</span><br><span class="line">        Select(<span class="string">&quot;id&quot;</span>, <span class="string">&quot;name&quot;</span>, <span class="string">&quot;age&quot;</span>, <span class="string">&quot;ctime&quot;</span>, <span class="string">&quot;mtime&quot;</span>).</span><br><span class="line">        From(<span class="string">&quot;user&quot;</span>).</span><br><span class="line">        Where(GT(<span class="string">&quot;id&quot;</span>, <span class="number">0</span>), GT(<span class="string">&quot;age&quot;</span>, <span class="number">0</span>)).</span><br><span class="line">        OrderBy(<span class="string">&quot;id&quot;</span>).</span><br><span class="line">        Limit(<span class="number">0</span>, <span class="number">20</span>).</span><br><span class="line">        Query()</span><br><span class="line">     </span><br><span class="line">    rows, err := db.QueryContext(ctx, sql, args...)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    result := []*User&#123;&#125;</span><br><span class="line">    err = ScanSlice(rows, &amp;result)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>生成的查询SQL语句和args如下：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> `id`,`name`,`age`,`ctime`,`mtime` <span class="keyword">FROM</span> `<span class="keyword">user</span>` <span class="keyword">WHERE</span> `id` <span class="operator">&gt;</span> ? <span class="keyword">AND</span> `age` <span class="operator">&gt;</span> ? <span class="keyword">ORDER</span> <span class="keyword">BY</span> id LIMIT <span class="number">20</span> <span class="keyword">OFFSET</span> <span class="number">0</span>  [<span class="number">0</span> <span class="number">0</span>]</span><br></pre></td></tr></table></figure><h2 id="自动生成">自动生成</h2><p>通过上面的使用的例子来看，我们的工作轻松了不少：</p><ul><li>第一：SQL语句不需要硬编码了；</li><li>第二：Scan不需要写大量结构体字段和的乏味的重复代码。</li></ul><p>着实帮我们省了很大的麻烦。但是查询字段还需要我们自己手写，像这种</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Select(<span class="string">&quot;id&quot;</span>, <span class="string">&quot;name&quot;</span>, <span class="string">&quot;age&quot;</span>, <span class="string">&quot;ctime&quot;</span>, <span class="string">&quot;mtime&quot;</span>).</span><br></pre></td></tr></table></figure><ul><li>其中传入的字段需要我们硬编码，我们可不可以再进一步，通过表结构定义来生成我们的golang结构体呢？答案是肯定的，要实现这一步我们需要一个SQL语句的<strong>解析器</strong>（<a href="https://github.com/xwb1989/sqlparser">https://github.com/xwb1989/sqlparser</a>)，把SQL DDL语句解析成go语言中如下的Table对象，其所包含的表名，列名、列类型、注释等都能获取到，再通过这些对象和写好的模板代码来生成我们实际业务使用的代码。</li></ul><p>Table对象如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">type</span> Table <span class="keyword">struct</span> &#123;</span><br><span class="line">    TableName   <span class="type">string</span>    <span class="comment">// table name</span></span><br><span class="line">    GoTableName <span class="type">string</span>    <span class="comment">// go struct name</span></span><br><span class="line">    PackageName <span class="type">string</span>    <span class="comment">// package name</span></span><br><span class="line">    Fields      []*Column <span class="comment">// columns</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> Column <span class="keyword">struct</span> &#123;</span><br><span class="line">    ColumnName    <span class="type">string</span> <span class="comment">// column_name</span></span><br><span class="line">    ColumnType    <span class="type">string</span> <span class="comment">// column_type</span></span><br><span class="line">    ColumnComment <span class="type">string</span> <span class="comment">// column_comment</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用以上Table对象的模板代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> &#123;&#123;.GoTableName&#125;&#125; <span class="keyword">struct</span> &#123;</span><br><span class="line">    &#123;&#123;- <span class="keyword">range</span> .Fields &#125;&#125;</span><br><span class="line">        &#123;&#123; .GoColumnName &#125;&#125; &#123;&#123;  .GoColumnType &#125;&#125; <span class="string">`json:&quot;&#123;&#123; .ColumnName &#125;&#125;&quot;`</span> <span class="comment">// &#123;&#123; .ColumnComment &#125;&#125;</span></span><br><span class="line">    &#123;&#123;- end&#125;&#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    table = <span class="string">&quot;&#123;&#123;.TableName&#125;&#125;&quot;</span></span><br><span class="line">    &#123;&#123;- <span class="keyword">range</span> .Fields&#125;&#125;</span><br><span class="line">        &#123;&#123; .GoColumnName&#125;&#125; = <span class="string">&quot;&#123;&#123;.ColumnName&#125;&#125;&quot;</span> </span><br><span class="line">    &#123;&#123;- end &#125;&#125;</span><br><span class="line">)</span><br><span class="line"><span class="keyword">var</span> columns = []<span class="type">string</span>&#123;</span><br><span class="line">    &#123;&#123;- <span class="keyword">range</span> .Fields&#125;&#125;</span><br><span class="line">    &#123;&#123; .GoColumnName&#125;&#125;,</span><br><span class="line">    &#123;&#123;- end &#125;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过上面的模板我们用user表的建表SQL语句生成如下代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> &#123;</span><br><span class="line">    Id    <span class="type">int64</span>     <span class="string">`json:&quot;id&quot;`</span>    <span class="comment">// id字段</span></span><br><span class="line">    Name  <span class="type">string</span>    <span class="string">`json:&quot;name&quot;`</span>  <span class="comment">// 名称</span></span><br><span class="line">    Age   <span class="type">int64</span>     <span class="string">`json:&quot;age&quot;`</span>   <span class="comment">// 年龄</span></span><br><span class="line">    Ctime time.Time <span class="string">`json:&quot;ctime&quot;`</span> <span class="comment">// 创建时间</span></span><br><span class="line">    Mtime time.Time <span class="string">`json:&quot;mtime&quot;`</span> <span class="comment">// 更新时间</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    table = <span class="string">&quot;user&quot;</span></span><br><span class="line">    Id = <span class="string">&quot;id&quot;</span></span><br><span class="line">    Name = <span class="string">&quot;name&quot;</span></span><br><span class="line">    Age = <span class="string">&quot;age&quot;</span></span><br><span class="line">    Ctime = <span class="string">&quot;ctime&quot;</span></span><br><span class="line">    Mtime = <span class="string">&quot;mtime&quot;</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">var</span> Columns = []<span class="type">string</span>&#123;<span class="string">&quot;id&quot;</span>,<span class="string">&quot;name&quot;</span>,<span class="string">&quot;age&quot;</span>,<span class="string">&quot;ctime&quot;</span>,<span class="string">&quot;mtime&quot;</span>&#125;</span><br></pre></td></tr></table></figure><p>那么我们在查询的时候就可以这样使用</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Select(Columns...)</span><br></pre></td></tr></table></figure><p>通过模板自动生成代码，可以大大的减轻开发编码负担，使我们从繁重的代码中解放出来。</p><h2 id="reflect真的有必要吗？">reflect真的有必要吗？</h2><p>由于我们SELECT时选择查找的字段和顺序是不固定的，我们有可能 SELECT id, name, age FROM user，也可能 SELECT name, id FROM user，有很大的任意性，这种情况使用反射出来的结构体tag和查询的列名来确定映射关系是必须的。但是有一种情况我们不需要用到反射，而且是一种最常用的情况，即：查询的字段名和表结构的列名一致，且顺序一致。这时候我们可以这么写，通过DeepEqual来判断查询字段和表结构字段是否一致且顺序一致来决定是否通过反射还是通过传统方法来创建对象。用传统方式创建对象（如下图第12行）令我们编码痛苦，不过可以通过模板来自动生成下面的代码，以避免手写，这样既灵活方便好用，性能又没有损耗，看起来是一个比较完美的解决方案。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">FindUserNoReflect</span><span class="params">(b *SelectBuilder)</span></span> ([]*User, <span class="type">error</span>) &#123;</span><br><span class="line">    sql, args := b.Query()</span><br><span class="line">    rows, err := db.QueryContext(ctx, sql, args...)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    result := []*User&#123;&#125;</span><br><span class="line">    <span class="keyword">if</span> DeepEqual(b.column, Columns) &#123;</span><br><span class="line">        <span class="keyword">defer</span> rows.Close()</span><br><span class="line">        <span class="keyword">for</span> rows.Next() &#123;</span><br><span class="line">            a := &amp;User&#123;&#125;</span><br><span class="line">            <span class="keyword">if</span> err := rows.Scan(&amp;a.Id, &amp;a.Name, &amp;a.Age, &amp;a.Ctime, &amp;a.Mtime); err != <span class="literal">nil</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">            &#125;</span><br><span class="line">            result = <span class="built_in">append</span>(result, a)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> rows.Err() != <span class="literal">nil</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">nil</span>, rows.Err()</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> result, <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line">    err = ScanSlice(rows, &amp;result)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结与展望">总结与展望</h2><h3 id="总结">总结</h3><ol><li>通过database/sql 库开发有较大痛点，ORM就是为了解决以上问题而生，其存在是有意义的。</li><li>ORM 两个关键的部分是SQLBuilder和Scanner的实现。</li><li>ORM Scanner 使用反射创建对象在性能上肯定会有一定的损失，但是带来极大的灵活性,同时在查询全表字段这种特殊情况下规避使用反射来提高性能。</li></ol><h3 id="展望">展望</h3><p>通过表结构，我们可以生成对应的结构体和持久层增删改查代码，我们再往前扩展一步，能否通过表结构生成的proto格式的message，以及一些常用的CRUD GRPC rpc接口定义。通过工具，我们甚至可以把前端的代码都生成好，实现半自动化编程。我想这个是值得期待的。</p><h2 id="参考资料">参考资料</h2><p><a href="https://github.com/ent/ent">https://github.com/ent/ent</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;为了提高开发效率和质量，我们常常需要ORM来帮助我们快速实现持久层增删改查API，目前go语言实现的ORM有很多种，他们都有自己的优劣点，有的实现简单，有的功能复杂，有的API十分优雅。在使用了多个类似的工具之后，总是会发现某些点无法满足解决我们生产环境中碰到的实际问题，比</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go项目多阶段构建Docker镜像</title>
    <link href="https://blog.mailjob.net/posts/1304110700.html"/>
    <id>https://blog.mailjob.net/posts/1304110700.html</id>
    <published>2022-08-01T14:29:42.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<p>在 Docker 17.05 版本之前，我们构建 Docker 镜像时，通常会采用两种方式：</p><h3 id="全部放入一个-Dockerfile">全部放入一个 Dockerfile</h3><p>一种方式是将所有的构建过程编包含在一个 <code>Dockerfile</code> 中，包括项目及其依赖库的编译、测试、打包等流程，这里可能会带来的一些问题：</p><ul><li>镜像层次多，镜像体积较大，部署时间变长</li><li>源代码存在泄露的风险</li></ul><p>例如，编写 <code>app.go</code> 文件，该程序输出 <code>Hello World!</code></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;fmt&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;Hello World!&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编写 <code>Dockerfile.one</code> 文件</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> golang:alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apk --no-cache add git ca-certificates</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /go/src/github.com/go/helloworld/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> app.go .</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> go get -d -v github.com/go-sql-driver/mysql \</span></span><br><span class="line"><span class="language-bash">  &amp;&amp; CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \</span></span><br><span class="line"><span class="language-bash">  &amp;&amp; <span class="built_in">cp</span> /go/src/github.com/go/helloworld/app /root</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /root/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;./app&quot;</span>]</span></span><br></pre></td></tr></table></figure><p>构建镜像</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker build -t go/helloworld:1 -f Dockerfile.one .</span><br></pre></td></tr></table></figure><h3 id="多阶段构建的优势">多阶段构建的优势</h3><p>自 Docker 17.05 版本起，引入了多阶段构建（multi-stage builds）的概念，这种方式能有效解决上述问题。多阶段构建允许我们在一个 <code>Dockerfile</code> 中定义多个 <code>FROM</code> 指令，从而在构建过程中生成多个临时镜像，这样可以减小最终镜像的体积，并避免将源代码暴露在生产环境中。</p><h3 id="多阶段构建的示例">多阶段构建的示例</h3><p>以下是一个使用多阶段构建的示例，演示如何构建一个更为精简的 Docker 镜像。</p><p>编写 <code>Dockerfile</code> 文件：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第一阶段：构建阶段</span></span><br><span class="line"><span class="keyword">FROM</span> golang:alpine AS builder</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apk --no-cache add git ca-certificates</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /go/src/github.com/go/helloworld/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> app.go .</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取依赖并构建</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> go get -d -v github.com/go-sql-driver/mysql \</span></span><br><span class="line"><span class="language-bash">  &amp;&amp; CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 第二阶段：运行阶段</span></span><br><span class="line"><span class="keyword">FROM</span> alpine:latest</span><br><span class="line"></span><br><span class="line"><span class="comment"># 复制构建好的二进制文件</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /root/</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=builder /go/src/github.com/go/helloworld/app .</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行应用程序</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;./app&quot;</span>]</span></span><br></pre></td></tr></table></figure><h3 id="构建镜像">构建镜像</h3><p>使用以下命令构建镜像：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker build -t go/helloworld:multi .</span><br></pre></td></tr></table></figure><h3 id="运行镜像">运行镜像</h3><p>使用以下命令运行生成的镜像：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker run --<span class="built_in">rm</span> go/helloworld:multi</span><br></pre></td></tr></table></figure><h3 id="总结">总结</h3><p>多阶段构建 Docker 镜像的方式，不仅可以优化镜像的体积，还能提高构建的安全性，避免将源代码暴露到最终镜像中。通过使用 <code>COPY --from=builder</code> 指令，我们可以轻松地将需要的文件从构建阶段复制到运行阶段，确保最终镜像中仅包含必要的二进制文件和资源。这种构建方式对于构建微服务和容器化应用特别有用，能够提升持续集成和持续部署的效率。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 Docker 17.05 版本之前，我们构建 Docker 镜像时，通常会采用两种方式：&lt;/p&gt;
&lt;h3 id=&quot;全部放入一个-Dockerfile&quot;&gt;全部放入一个 Dockerfile&lt;/h3&gt;
&lt;p&gt;一种方式是将所有的构建过程编包含在一个 &lt;code&gt;Dockerf</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go 环境搭建</title>
    <link href="https://blog.mailjob.net/posts/2723567567.html"/>
    <id>https://blog.mailjob.net/posts/2723567567.html</id>
    <published>2022-06-22T14:59:03.000Z</published>
    <updated>2026-07-25T09:01:29.898Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景">背景</h2><p>Go语言在 v1.14 之前使用 go path 模式，在 v1.14 之后采用 go mod 模式管理项目。该文章针对 v1.14 后的环境安装方式进行讲解。</p><h2 id="Mac系统安装">Mac系统安装</h2><p>MacOs 建议使用 <a href="https://brew.sh/index_zh-cn">brew</a> 包管理工具安装go语言环境，运行以下命令安装go</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install go</span><br></pre></td></tr></table></figure><h3 id="配置环境变量">配置环境变量</h3><p><strong>进入配置文件</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># bash 终端</span></span><br><span class="line">$ vim ~/.bash_profile</span><br><span class="line"><span class="comment"># zsh 终端</span></span><br><span class="line">$ vim ~/.zshrc</span><br></pre></td></tr></table></figure><p><strong>配置环境变量</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export GO111MODULE=on</span><br><span class="line">export GOPATH=$HOME/go</span><br><span class="line">export GOBIN=$GOPATH/bin</span><br><span class="line">export GOPROXY=https://goproxy.cn,direct</span><br><span class="line">export PATH=$PATH:$GOBIN</span><br></pre></td></tr></table></figure><p><strong>配置参数说明</strong></p><ul><li>GO111MODULE：go mod 模式，v1.14后默认 auto，常见配置为：auto、on、off，建议配置为 on</li><li>GOPATH：golang安装目录</li><li>GOBIN：golang执行目录</li><li>GOPROXY：配置国内镜像加速</li><li>PATH：配置环境变量</li></ul><p><strong>刷新环境变量</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># bash 终端</span></span><br><span class="line">$ <span class="built_in">source</span> ~/.bash_profile</span><br><span class="line"><span class="comment"># zsh 终端</span></span><br><span class="line">$ <span class="built_in">source</span> ~/.zshrc</span><br></pre></td></tr></table></figure><p><strong>查看配置</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">$ go <span class="built_in">env</span></span><br><span class="line">GO111MODULE=<span class="string">&quot;on&quot;</span></span><br><span class="line">GOARCH=<span class="string">&quot;amd64&quot;</span></span><br><span class="line">GOBIN=<span class="string">&quot;/Users/libin/go/bin&quot;</span></span><br><span class="line">GOCACHE=<span class="string">&quot;/Users/libin/Library/Caches/go-build&quot;</span></span><br><span class="line">GOENV=<span class="string">&quot;/Users/libin/Library/Application Support/go/env&quot;</span></span><br><span class="line">GOEXE=<span class="string">&quot;&quot;</span></span><br><span class="line">GOEXPERIMENT=<span class="string">&quot;&quot;</span></span><br><span class="line">GOFLAGS=<span class="string">&quot;&quot;</span></span><br><span class="line">GOHOSTARCH=<span class="string">&quot;amd64&quot;</span></span><br><span class="line">GOHOSTOS=<span class="string">&quot;darwin&quot;</span></span><br><span class="line">GOINSECURE=<span class="string">&quot;&quot;</span></span><br><span class="line">GOMODCACHE=<span class="string">&quot;/Users/libin/go/pkg/mod&quot;</span></span><br><span class="line">GONOPROXY=<span class="string">&quot;&quot;</span></span><br><span class="line">GONOSUMDB=<span class="string">&quot;&quot;</span></span><br><span class="line">GOOS=<span class="string">&quot;darwin&quot;</span></span><br><span class="line">GOPATH=<span class="string">&quot;/Users/libin/go&quot;</span></span><br><span class="line">GOPRIVATE=<span class="string">&quot;&quot;</span></span><br><span class="line">GOPROXY=<span class="string">&quot;https://goproxy.cn,direct&quot;</span></span><br><span class="line">GOROOT=<span class="string">&quot;/usr/local/Cellar/go/1.18.3/libexec&quot;</span></span><br><span class="line">GOSUMDB=<span class="string">&quot;sum.golang.org&quot;</span></span><br><span class="line">GOTMPDIR=<span class="string">&quot;&quot;</span></span><br><span class="line">GOTOOLDIR=<span class="string">&quot;/usr/local/Cellar/go/1.18.3/libexec/pkg/tool/darwin_amd64&quot;</span></span><br><span class="line">GOVCS=<span class="string">&quot;&quot;</span></span><br><span class="line">GOVERSION=<span class="string">&quot;go1.18.3&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 省略 ... ...</span></span><br></pre></td></tr></table></figure><h2 id="Windows系统安装">Windows系统安装</h2><p>Go SDK 下载地址：<a href="https://golang.google.cn/dl/">https://golang.google.cn/dl/</a></p><table><thead><tr><th>File name</th><th>Kind</th><th>OS</th><th>Arch</th><th>Size</th><th>SHA256 Checksum</th></tr></thead><tbody><tr><td>go1.18.3.windows-amd64.msi</td><td>Installer</td><td>Windows</td><td>X86-64</td><td>130MB</td><td>692ee6225305ad909630c9cc152719a9bdb332e911d180cf3143a5b6a09cc863</td></tr></tbody></table><p>请根据自己的电脑芯片下载合适的版本，我的机器是 x86 芯片 64 位系统，所以我就下载此版本的软件了。</p><p>下载完成后，和其他应用类软件一样，需要选择一个你喜欢的目录安装此软件，接下来开始配置环境变量。</p><h3 id="配置环境变量-2">配置环境变量</h3><p>此电脑(右键属性) --&gt; 关于 --&gt; 高级系统设置 --&gt; 环境变量 --&gt; 新建</p><p><strong>建立以下环境变量</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">变量：GO111MODULE值：on</span><br><span class="line">变量：GOROOT值：E:\Go</span><br><span class="line">变量：GOPROXY值：https://goproxy.cn,direct</span><br></pre></td></tr></table></figure><p><strong>选择 <code>PATH</code>，点击编辑，点击新建，添加PATH环境变量</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%GOROOT%\bin;</span><br></pre></td></tr></table></figure><p><strong>打开 cmd 窗口，输入 <code>go env</code> ，测试是否配置成功</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">C:\Users\58850&gt;go <span class="built_in">env</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;
&lt;p&gt;Go语言在 v1.14 之前使用 go path 模式，在 v1.14 之后采用 go mod 模式管理项目。该文章针对 v1.14 后的环境安装方式进行讲解。&lt;/p&gt;
&lt;h2 id=&quot;Mac系统安装&quot;&gt;Mac系统安装&lt;/h2&gt;
&lt;p</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go Interface 合法验证</title>
    <link href="https://blog.mailjob.net/posts/2809645964.html"/>
    <id>https://blog.mailjob.net/posts/2809645964.html</id>
    <published>2022-05-15T11:12:19.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<h2 id="接口实现判断依据">接口实现判断依据</h2><ul><li>值方法集和接口匹配<ul><li>给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集</li></ul></li><li>指针方法集和接口匹配<ul><li>只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配</li><li>如果将值对象赋值给接口变量,会在编译期报错(会触发接口合理性检查机制)</li></ul></li></ul><h3 id="接口绑定">接口绑定</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Annimaler <span class="keyword">interface</span> &#123;</span><br><span class="line">Name() <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Dog <span class="keyword">struct</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(d *Dog)</span></span> Name() <span class="type">string</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="string">&quot;二哈&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>  &#123;</span><br><span class="line"><span class="keyword">var</span> i Annimaler</span><br><span class="line">i = &amp;Dog&#123;&#125;</span><br><span class="line">i = (*Dog)(<span class="literal">nil</span>)</span><br><span class="line">    i = <span class="built_in">new</span>(Dog)</span><br><span class="line"><span class="comment">// i = Handle&#123;&#125; // 无法编译通过，因为 i 是指针类型，Handle&#123;&#125; 不是指针类型</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用接口方法</span></span><br><span class="line">fmt.Println(i.Name())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>基于以上的代码，我们大致可以写出 go interface 合理性验证的代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> _ Annimaler = &amp;Dog&#123;&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> _ Annimaler = (*Dog)(<span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p><strong>代码解释</strong></p><p>赋值的右边应该是断言类型的零值，也就是说 Dog 类型的零值等于Annimaler类型的零值。<br>如果是指针类型（如 *Annimaler）、切片和映射，这是 <code>nil</code>；<br>如果是结构类型，这是空结构。</p><blockquote><p>(*Dog)(nil) 是类型断言，就是把变量用 <code>nil</code> 代替，把 <code>nil</code> 转换成一个  <code>Dog</code> 类型的空指针后赋值给 <code>Annimaler</code></p></blockquote><h3 id="接收器-receiver-与接口，接口与具体方法集的匹配">接收器 (receiver) 与接口，接口与具体方法集的匹配</h3><p><strong>一个类型可以有值接收器方法集和指针接收器方法集</strong></p><p>使用值接收器的方法既可以通过值调用，也可以通过指针调用。带指针接收器的方法只能通过指针或 addressable values调用(其实和值调用类似)。</p><p>如果方法的接收者是值类型，无论调用者是对象还是对象指针，修改的都是对象的副本，不影响调用者；如果方法的接收者是指针类型，则调用者修改的是指针指向的对象本身。</p><p>通常我们使用指针作为方法的接收者的理由：</p><ul><li>使用指针方法能够修改接收者指向的值。</li><li>可以避免在每次调用方法时复制该值，在值的类型为大型结构体时，这样做会更加高效。</li></ul><p>在该代码中：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">i = &amp;Dog&#123;&#125;</span><br><span class="line">fmt.Println(i.Name())</span><br></pre></td></tr></table></figure><p>编译是可以通过的，这就是通过 addressable values 调用的。</p><h2 id="参考资料">参考资料</h2><ul><li>uber-go/guide的中文翻译：<a href="https://github.com/xxjwxc/uber_go_guide_cn">https://github.com/xxjwxc/uber_go_guide_cn</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;接口实现判断依据&quot;&gt;接口实现判断依据&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;值方法集和接口匹配
&lt;ul&gt;
&lt;li&gt;给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;指针方法集和接口匹配
&lt;ul&gt;
&lt;li&gt;只能将指针对</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go安装可执行工具</title>
    <link href="https://blog.mailjob.net/posts/1692897008.html"/>
    <id>https://blog.mailjob.net/posts/1692897008.html</id>
    <published>2022-05-11T02:07:34.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言">前言</h2><p>在 Go 语言中，我们通常用借用一些工具作为可执行程序使用。但是按照官方文档安装过程中，总是会发生 <code>go get</code> 成功了，但是执行命令的时候总是提示命令未找到的错误，例如：<code>Mac: bash: /Users/libin/go/bin/xxx: No such file or directory</code>、<code>Windows: xxx不是内部或外部命令，也不是可运行的程序或批处理文件</code></p><p>在常用的工具中，拿 <a href="https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go">protoc-gen-go</a> 和 <a href="https://github.com/google/wire">wire</a> 来举例，需要文档的安装命令是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go get github.com/google/wire/cmd/wire</span><br></pre></td></tr></table></figure><p>如果你在非 <code>go.mod</code> 执行该程序会提示找不到 <code>go.mod</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></pre></td><td class="code"><pre><span class="line">localhost:bin libin$ go get github.com/google/wire/cmd/wire</span><br><span class="line">go: go.mod file not found <span class="keyword">in</span> current directory or any parent directory.</span><br><span class="line">        <span class="string">&#x27;go get&#x27;</span> is no longer supported outside a module.</span><br><span class="line">        To build and install a <span class="built_in">command</span>, use <span class="string">&#x27;go install&#x27;</span> with a version,</span><br><span class="line">        like <span class="string">&#x27;go install example.com/cmd@latest&#x27;</span></span><br><span class="line">        For more information, see https://golang.org/doc/go-get-install-deprecation</span><br><span class="line">        or run <span class="string">&#x27;go help get&#x27;</span> or <span class="string">&#x27;go help install&#x27;</span>.</span><br></pre></td></tr></table></figure><p>如果你在 <code>go.mod</code> 目录执行该命令，当然可以运行成功。但是问题是，虽然成功了，但是你无法在任意目录执行该应用程序，例如：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">localhost:bin libin$ wire</span><br><span class="line">bash: /Users/libin/go/bin/wire: No such file or directory</span><br></pre></td></tr></table></figure><h2 id="解决过程">解决过程</h2><h3 id="原因分析">原因分析</h3><p>在 Go 1.3 之前，由于采用的是 GOPATH 模式，用以上的安装方法当然没问题，因为下载的包，最后会编译为可执行程序后安装到项目的bin目录。</p><p>在 Go 1.4 之后，采用了 go mod 模式，如果使用 go get 安装，则无法安装到执行的 bin 目录，所以我们要做的是想办法让程序安装到 <code>$GOPATH/bin</code> 目录，这样你就可以在全局使用该执行程序了。</p><blockquote><p>小贴士：</p><p>可用 <code>go env</code> 命令查看更多 go 配置信息，包括 go 的环境变量配置。建议将<code>$GOPATH/bin</code>加入系统环境变量<code>$PATH</code>中。</p></blockquote><h3 id="解决方案">解决方案</h3><p>在任意目录处，使用 install 安装程序</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go install github.com/google/wire/cmd/wire@latest</span><br></pre></td></tr></table></figure><p>上面的命令会在<code>$GOPATH/bin</code>中生成一个可执行程序<code>wire</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></pre></td><td class="code"><pre><span class="line">localhost:~ libin$ wire <span class="built_in">help</span></span><br><span class="line">Usage: wire &lt;flags&gt; &lt;subcommand&gt; &lt;subcommand args&gt;</span><br><span class="line"></span><br><span class="line">Subcommands:</span><br><span class="line">        check            <span class="built_in">print</span> any Wire errors found</span><br><span class="line">        commands         list all <span class="built_in">command</span> names</span><br><span class="line">        diff             output a diff between existing wire_gen.go files and what gen would generate</span><br><span class="line">        flags            describe all known top-level flags</span><br><span class="line">        gen              generate the wire_gen.go file <span class="keyword">for</span> each package</span><br><span class="line">        <span class="built_in">help</span>             describe subcommands and their syntax</span><br><span class="line">        show             describe all top-level provider sets</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Use <span class="string">&quot;wire flags&quot;</span> <span class="keyword">for</span> a list of top-level flags</span><br></pre></td></tr></table></figure><h3 id="补充知识">补充知识</h3><p><strong>go install 和 go get 的区别</strong></p><ul><li><code>go install</code> 命令可以接受一个版本后缀了，（例如，go install <a href="http://sigs.k8s.io/kind@v0.9.0%EF%BC%89%EF%BC%8C%E5%B9%B6%E4%B8%94%E5%AE%83%E6%98%AF%E5%9C%A8%E6%A8%A1%E5%9D%97%E6%84%9F%E7%9F%A5%E7%9A%84%E6%A8%A1%E5%BC%8F%E4%B8%8B%E8%BF%90%E8%A1%8C%EF%BC%8C%E5%8F%AF%E5%BF%BD%E7%95%A5%E5%BD%93%E5%89%8D%E7%9B%AE%E5%BD%95%E6%88%96%E4%B8%8A%E5%B1%82%E7%9B%AE%E5%BD%95%E7%9A%84">sigs.k8s.io/kind@v0.9.0），并且它是在模块感知的模式下运行，可忽略当前目录或上层目录的</a> <code>go.mod</code> 文件。这对于在不影响主模块依赖的情况下，安装二进制很方便</li><li><code>go install</code> 被设计为“用于构建和安装二进制文件”， <code>go get</code> 则被设计为 “用于编辑 go.mod 变更依赖”</li></ul><p><strong>go get 其他参数介绍</strong></p><ul><li>-d 只下载不安装</li><li>-f 只有在你包含了 -u 参数的时候才有效，不让 -u 去验证 import 中的每一个都已经获取了，这对于本地 fork 的包特别有用</li><li>-fix 在获取源码之后先运行 fix，然后再去做其他的事情</li><li>-t 同时也下载需要为运行测试所需要的包</li><li><strong>-u 强制使用网络去更新包和它的依赖包</strong></li><li>-v 显示执行的命令</li></ul><p>原文链接：<a href="https://www.yipwinghong.com/2021/12/10/Go_engineering-specification-design">https://www.yipwinghong.com/2021/12/10/Go_engineering-specification-design</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;在 Go 语言中，我们通常用借用一些工具作为可执行程序使用。但是按照官方文档安装过程中，总是会发生 &lt;code&gt;go get&lt;/code&gt; 成功了，但是执行命令的时候总是提示命令未找到的错误，例如：&lt;code&gt;Mac: bash: /</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Nginx基础知识总结</title>
    <link href="https://blog.mailjob.net/posts/31352437.html"/>
    <id>https://blog.mailjob.net/posts/31352437.html</id>
    <published>2022-05-01T10:06:51.000Z</published>
    <updated>2026-07-25T09:01:29.899Z</updated>
    
    <content type="html"><![CDATA[<meta name="referrer" content="no-referrer" /><h2 id="Nginx-概述">Nginx 概述</h2><p><img src="https://img-blog.csdnimg.cn/20210508090822890.png" alt="在这里插入图片描述"></p><p><strong>Nginx</strong> 是开源、高性能、高可靠的 <strong>Web</strong> 和<a href="https://so.csdn.net/so/search?q=%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86&amp;spm=1001.2101.3001.7020">反向代理</a>服务器，而且支持热部署，几乎可以做到 7 * 24 小时不间断运行，即使运行几个月也不需要重新启动，还能在不间断服务的情况下对软件版本进行热更新。性能是 <strong>Nginx</strong> 最重要的考量，其占用<a href="https://so.csdn.net/so/search?q=%E5%86%85%E5%AD%98&amp;spm=1001.2101.3001.7020">内存</a>少、并发能力强、能支持高达 5w 个并发连接数，最重要的是， <strong>Nginx</strong> 是免费的并可以商业化，配置使用也比较简单。</p><h2 id="Nginx-特点">Nginx 特点</h2><ul><li>高并发、高性能；</li><li>模块化架构使得它的扩展性非常好；</li><li>异步非阻塞的事件驱动模型这点和 Node.js 相似；</li><li>相对于其它服务器来说它可以连续几个月甚至更长而不需要重启服务器使得它具有高可靠性；</li><li>热部署、平滑升级；</li><li>完全开源，生态繁荣；</li></ul><h2 id="Nginx-作用">Nginx 作用</h2><p>Nginx 的最重要的几个使用场景：</p><ol><li>静态资源服务，通过本地文件系统提供服务；</li><li>反向代理服务，延伸出包括缓存、负载均衡等；</li><li>API 服务， OpenResty ；</li></ol><p>对于前端来说 Node.js 并不陌生， Nginx 和 Node.js 的很多理念类似， HTTP 服务器、事件驱动、异步非阻塞等，且 Nginx 的大部分功能使用 Node.js 也可以实现，但 Nginx 和 Node.js 并不冲突，都有自己擅长的领域。 Nginx 擅长于底层服务器端资源的处理（静态资源处理转发、反向代理，负载均衡等）， Node.js 更擅长上层具体业务逻辑的处理，两者可以完美组合。<br>用一张图表示：<br><img src="https://img-blog.csdnimg.cn/20210508091444609.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p><h2 id="Nginx-安装">Nginx 安装</h2><p>本文演示的是 Linux centOS 7.x 的操作系统上安装 Nginx ，至于在其它操作系统上进行安装可以网上自行搜索，都非常简单的。</p><p>使用 yum 安装 Nginx ：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install nginx <span class="literal">-y</span></span><br></pre></td></tr></table></figure><p>安装完成后，通过 <code>rpm -ql nginx</code>命令查看 Nginx 的安装信息：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Nginx配置文件</span></span><br><span class="line">/etc/nginx/nginx.conf <span class="comment"># nginx 主配置文件</span></span><br><span class="line">/etc/nginx/nginx.conf.default</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可执行程序文件</span></span><br><span class="line">/usr/bin/nginx<span class="literal">-upgrade</span></span><br><span class="line">/usr/sbin/nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># nginx库文件</span></span><br><span class="line">/usr/lib/systemd/system/nginx.service <span class="comment"># 用于配置系统守护进程</span></span><br><span class="line">/usr/lib64/nginx/modules <span class="comment"># Nginx模块目录</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 帮助文档</span></span><br><span class="line">/usr/share/doc/nginx<span class="literal">-1</span>.<span class="number">16.1</span></span><br><span class="line">/usr/share/doc/nginx<span class="literal">-1</span>.<span class="number">16.1</span>/CHANGES</span><br><span class="line">/usr/share/doc/nginx<span class="literal">-1</span>.<span class="number">16.1</span>/README</span><br><span class="line">/usr/share/doc/nginx<span class="literal">-1</span>.<span class="number">16.1</span>/README.dynamic</span><br><span class="line">/usr/share/doc/nginx<span class="literal">-1</span>.<span class="number">16.1</span>/UPGRADE<span class="literal">-NOTES-1</span>.<span class="number">6</span><span class="literal">-to-1</span>.<span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 静态资源目录</span></span><br><span class="line">/usr/share/nginx/html/<span class="number">404</span>.html</span><br><span class="line">/usr/share/nginx/html/<span class="number">50</span>x.html</span><br><span class="line">/usr/share/nginx/html/index.html</span><br><span class="line"></span><br><span class="line"><span class="comment"># 存放Nginx日志文件</span></span><br><span class="line">/var/log/nginx</span><br></pre></td></tr></table></figure><p>主要关注的文件夹有两个：</p><ol><li><code>/etc/nginx/conf.d/</code> 是子配置项存放处， <code>/etc/nginx/nginx.conf</code><br>主配置文件会默认把这个文件夹中所有子配置项都引入；</li><li><code>/usr/share/nginx/html/</code> 静态文件都放在这个文件夹，也可以根据你自己的习惯放在其他地方；</li></ol><h2 id="Nginx-常用命令">Nginx 常用命令</h2><p><strong>systemctl</strong> 系统命令：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 开机配置</span></span><br><span class="line">systemctl enable nginx <span class="comment"># 开机自动启动</span></span><br><span class="line">systemctl disable nginx <span class="comment"># 关闭开机自动启动</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动Nginx</span></span><br><span class="line">systemctl <span class="built_in">start</span> nginx <span class="comment"># 启动Nginx成功后，可以直接访问主机IP，此时会展示Nginx默认页面</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 停止Nginx</span></span><br><span class="line">systemctl stop nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重启Nginx</span></span><br><span class="line">systemctl restart nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重新加载Nginx</span></span><br><span class="line">systemctl reload nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看 Nginx 运行状态</span></span><br><span class="line">systemctl status nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看Nginx进程</span></span><br><span class="line"><span class="built_in">ps</span> <span class="literal">-ef</span> | grep nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 杀死Nginx进程</span></span><br><span class="line"><span class="built_in">kill</span> <span class="literal">-9</span> pid <span class="comment"># 根据上面查看到的Nginx进程号，杀死Nginx进程，-9 表示强制结束进程</span></span><br></pre></td></tr></table></figure><p><strong>Nginx</strong> 应用程序命令：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">nginx <span class="literal">-s</span> reload  <span class="comment"># 向主进程发送信号，重新加载配置文件，热重启</span></span><br><span class="line">nginx <span class="literal">-s</span> reopen <span class="comment"># 重启 Nginx</span></span><br><span class="line">nginx <span class="literal">-s</span> stop    <span class="comment"># 快速关闭</span></span><br><span class="line">nginx <span class="literal">-s</span> quit    <span class="comment"># 等待工作进程处理完成后关闭</span></span><br><span class="line">nginx <span class="literal">-T</span>         <span class="comment"># 查看当前 Nginx 最终的配置</span></span><br><span class="line">nginx <span class="literal">-t</span>         <span class="comment"># 检查配置是否有问题</span></span><br></pre></td></tr></table></figure><h2 id="Nginx-核心配置">Nginx 核心配置</h2><h3 id="配置文件结构">配置文件结构</h3><p>Nginx 的典型配置示例：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># main段配置信息</span></span><br><span class="line">user  nginx<span class="comment">;                        # 运行用户，默认即是nginx，可以不进行设置</span></span><br><span class="line">worker_processes  auto<span class="comment">;             # Nginx 进程数，一般设置为和 CPU 核数一样</span></span><br><span class="line">error_log  /var/log/nginx/error.log warn<span class="comment">;   # Nginx 的错误日志存放目录</span></span><br><span class="line">pid        /var/run/nginx.pid<span class="comment">;      # Nginx 服务启动时的 pid 存放位置</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># events段配置信息</span></span><br><span class="line">events &#123;</span><br><span class="line">    use epoll<span class="comment">;     # 使用epoll的I/O模型(如果你不知道Nginx该使用哪种轮询方法，会自动选择一个最适合你操作系统的)</span></span><br><span class="line">    worker_connections 1024<span class="comment">;   # 每个进程允许最大并发数</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># http段配置信息</span></span><br><span class="line"><span class="comment"># 配置使用最频繁的部分，代理、缓存、日志定义等绝大多数功能和第三方模块的配置都在这里设置</span></span><br><span class="line">http &#123; </span><br><span class="line">    <span class="comment"># 设置日志模式</span></span><br><span class="line">    log_format  main  &#x27;$remote_addr - $remote_user <span class="section">[$time_local]</span> &quot;$request&quot; &#x27;</span><br><span class="line">                      &#x27;$status $body_bytes_sent &quot;$http_referer&quot; &#x27;</span><br><span class="line">                      &#x27;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#x27;<span class="comment">;</span></span><br><span class="line"></span><br><span class="line">    access_log  /var/log/nginx/access.log  main<span class="comment">;   # Nginx访问日志存放位置</span></span><br><span class="line"></span><br><span class="line">    sendfile            on<span class="comment">;   # 开启高效传输模式</span></span><br><span class="line">    tcp_nopush          on<span class="comment">;   # 减少网络报文段的数量</span></span><br><span class="line">    tcp_nodelay         on<span class="comment">;</span></span><br><span class="line">    keepalive_timeout   65<span class="comment">;   # 保持连接的时间，也叫超时时间，单位秒</span></span><br><span class="line">    types_hash_max_size 2048<span class="comment">;</span></span><br><span class="line"></span><br><span class="line">    include             /etc/nginx/mime.types<span class="comment">;      # 文件扩展名与类型映射表</span></span><br><span class="line">    default_type        application/octet-stream<span class="comment">;   # 默认文件类型</span></span><br><span class="line"></span><br><span class="line">    include /etc/nginx/conf.d/*.conf<span class="comment">;   # 加载子配置项</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># server段配置信息</span></span><br><span class="line">    server &#123;</span><br><span class="line">    listen       80<span class="comment">;       # 配置监听的端口</span></span><br><span class="line">    server_name  localhost<span class="comment">;    # 配置的域名</span></span><br><span class="line">      </span><br><span class="line">    <span class="comment"># location段配置信息</span></span><br><span class="line">    location / &#123;</span><br><span class="line">    root   /usr/share/nginx/html<span class="comment">;  # 网站根目录</span></span><br><span class="line">    index  index.html index.htm<span class="comment">;   # 默认首页文件</span></span><br><span class="line">    deny 172.168.22.11<span class="comment">;   # 禁止访问的ip地址，可以为all</span></span><br><span class="line">    allow 172.168.33.44；<span class="comment"># 允许访问的ip地址，可以为all</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    error_page 500 502 503 504 /50x.html<span class="comment">;  # 默认50x对应的访问页面</span></span><br><span class="line">    error_page 400 404 error.html<span class="comment">;   # 同上</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>main 全局配置，对全局生效；</li><li>events 配置影响 Nginx 服务器与用户的网络连接；</li><li>http 配置代理，缓存，日志定义等绝大多数功能和第三方模块的配置；</li><li>server 配置虚拟主机的相关参数，一个 http 块中可以有多个 server 块；</li><li>location 用于配置匹配的 uri ；</li><li>upstream 配置后端服务器具体地址，负载均衡配置不可或缺的部分；</li></ul><p>用一张图清晰的展示它的层级结构：<br><img src="https://img-blog.csdnimg.cn/20210508093616184.png" alt="在这里插入图片描述"></p><h3 id="配置文件-main-段核心参数">配置文件 main 段核心参数</h3><h4 id="user">user</h4><p>指定运行 Nginx 的 woker 子进程的属主和属组，其中组可以不指定。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">user USERNAME [<span class="type">GROUP</span>]</span><br><span class="line"></span><br><span class="line">user nginx lion; <span class="comment"># 用户是nginx;组是lion</span></span><br></pre></td></tr></table></figure><h4 id="pid">pid</h4><p>指定运行 Nginx master 主进程的 pid 文件存放路径。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pid /opt/nginx/logs/nginx.pid <span class="comment"># master主进程的的pid存放在nginx.pid的文件</span></span><br></pre></td></tr></table></figure><h4 id="worker-rlimit-nofile-number">worker_rlimit_nofile_number</h4><p>指定 worker 子进程可以打开的最大文件句柄数。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">worker_rlimit_nofile <span class="number">20480</span>; <span class="comment"># 可以理解成每个worker子进程的最大连接数量。</span></span><br></pre></td></tr></table></figure><h4 id="worker-rlimit-core">worker_rlimit_core</h4><p>指定 worker 子进程异常终止后的 core 文件，用于记录分析问题。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">worker_rlimit_core <span class="number">50</span>M; <span class="comment"># 存放大小限制</span></span><br><span class="line">working_directory /opt/nginx/tmp; <span class="comment"># 存放目录</span></span><br></pre></td></tr></table></figure><h4 id="worker-processes-number">worker_processes_number</h4><p>指定 Nginx 启动的 worker 子进程数量。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">worker_processes <span class="number">4</span>; <span class="comment"># 指定具体子进程数量</span></span><br><span class="line">worker_processes auto; <span class="comment"># 与当前cpu物理核心数一致</span></span><br></pre></td></tr></table></figure><h4 id="worker-cpu-affinity">worker_cpu_affinity</h4><p>将每个 worker 子进程与我们的 cpu 物理核心绑定。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">worker_cpu_affinity <span class="number">0001</span> <span class="number">0010</span> <span class="number">0100</span> <span class="number">1000</span>; <span class="comment"># 4个物理核心，4个worker子进程</span></span><br></pre></td></tr></table></figure><p><img src="https://img-blog.csdnimg.cn/20210508093924221.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>将每个 worker 子进程与特定 CPU 物理核心绑定，优势在于，避免同一个 worker 子进程在不同的 CPU 核心上切换，缓存失效，降低性能。但其并不能真正的避免进程切换。</p><h4 id="worker-priority">worker_priority</h4><p>指定 worker 子进程的 nice 值，以调整运行 Nginx 的优先级，通常设定为负值，以优先调用 Nginx 。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">worker_priority <span class="literal">-10</span>; <span class="comment"># 120-10=110，110就是最终的优先级</span></span><br></pre></td></tr></table></figure><p>Linux 默认进程的优先级值是120，值越小越优先； nice 定范围为 -20 到 +19 。<br>[备注] 应用的默认优先级值是120加上 nice 值等于它最终的值，这个值越小，优先级越高。</p><h4 id="worker-shutdown-timeout">worker_shutdown_timeout</h4><p>指定 worker 子进程优雅退出时的超时时间。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">worker_shutdown_timeout <span class="number">5</span>s;</span><br></pre></td></tr></table></figure><h4 id="timer-resolution">timer_resolution</h4><p>worker 子进程内部使用的计时器精度，调整时间间隔越大，系统调用越少，有利于性能提升；反之，系统调用越多，性能下降。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">timer_resolution <span class="number">100</span>ms;</span><br></pre></td></tr></table></figure><p>在 Linux 系统中，用户需要获取计时器时需要向操作系统内核发送请求，有请求就必然会有开销，因此这个间隔越大开销就越小。</p><h4 id="daemon">daemon</h4><p>指定 Nginx 的运行方式，前台还是后台，前台用于调试，后台用于生产。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">daemon off; <span class="comment"># 默认是on，后台运行模式</span></span><br></pre></td></tr></table></figure><h3 id="配置文件-events-段核心参数">配置文件 events 段核心参数</h3><h4 id="use">use</h4><p>Nginx 使用何种事件驱动模型。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">use method; <span class="comment"># 不推荐配置它，让nginx自己选择</span></span><br><span class="line"></span><br><span class="line">method 可选值为：<span class="built_in">select</span>、poll、kqueue、epoll、/dev/poll、eventport</span><br></pre></td></tr></table></figure><h4 id="worker-connections">worker_connections</h4><p>worker 子进程能够处理的最大并发连接数。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">worker_connections <span class="number">1024</span> <span class="comment"># 每个子进程的最大连接数为1024</span></span><br></pre></td></tr></table></figure><h4 id="accept-mutex">accept_mutex</h4><p>是否打开负载均衡互斥锁。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">accept_mutex on <span class="comment"># 默认是off关闭的，这里推荐打开</span></span><br></pre></td></tr></table></figure><h3 id="server-name-指令">server_name 指令</h3><p>指定虚拟主机域名。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">server_name name1 name2 name3</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例：</span></span><br><span class="line">server_name www.nginx.com;</span><br></pre></td></tr></table></figure><p>域名匹配的四种写法：</p><ul><li>精确匹配： server_name <a href="http://www.nginx.com">www.nginx.com</a> ;</li><li>左侧通配： server_name *.nginx.com ;</li><li>右侧统配： server_name www.nginx.* ;</li><li>正则匹配： server_name ~^www.nginx.*$ ;</li></ul><p>匹配优先级**：精确匹配 &gt; 左侧通配符匹配 &gt; 右侧通配符匹配 &gt; 正则表达式匹配**<br>server_name 配置实例：<br>1、配置本地 DNS 解析 vim /etc/hosts （ macOS 系统）</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 添加如下内容，其中 121.42.11.34 是阿里云服务器IP地址</span></span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> www.nginx<span class="literal">-test</span>.com</span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> mail.nginx<span class="literal">-test</span>.com</span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> www.nginx<span class="literal">-test</span>.org</span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> doc.nginx<span class="literal">-test</span>.com</span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> www.nginx<span class="literal">-test</span>.cn</span><br><span class="line"><span class="number">121.42</span>.<span class="number">11.34</span> fe.nginx<span class="literal">-test</span>.club</span><br></pre></td></tr></table></figure><p>[注意] 这里使用的是虚拟域名进行测试，因此需要配置本地 DNS 解析，如果使用阿里云上购买的域名，则需要在阿里云上设置好域名解析。<br>2、配置阿里云 Nginx ，vim /etc/nginx/nginx.conf</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 这里只列举了http端中的sever端配置</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 左匹配</span></span><br><span class="line">server &#123;</span><br><span class="line">listen<span class="number">80</span>;</span><br><span class="line">server_name*.nginx<span class="literal">-test</span>.com;</span><br><span class="line">root/usr/share/nginx/html/nginx<span class="literal">-test</span>/left<span class="operator">-match</span>/;</span><br><span class="line">location / &#123;</span><br><span class="line">index index.html;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 正则匹配</span></span><br><span class="line">server &#123;</span><br><span class="line">listen<span class="number">80</span>;</span><br><span class="line">server_name~^.*\.nginx<span class="literal">-test</span>\..*<span class="variable">$</span>;</span><br><span class="line">root/usr/share/nginx/html/nginx<span class="literal">-test</span>/reg<span class="operator">-match</span>/;</span><br><span class="line">location / &#123;</span><br><span class="line">index index.html;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 右匹配</span></span><br><span class="line">server &#123;</span><br><span class="line">listen<span class="number">80</span>;</span><br><span class="line">server_namewww.nginx<span class="literal">-test</span>.*;</span><br><span class="line">root/usr/share/nginx/html/nginx<span class="literal">-test</span>/right<span class="operator">-match</span>/;</span><br><span class="line">location / &#123;</span><br><span class="line">index index.html;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 完全匹配</span></span><br><span class="line">server &#123;</span><br><span class="line">listen<span class="number">80</span>;</span><br><span class="line">server_namewww.nginx<span class="literal">-test</span>.com;</span><br><span class="line">root/usr/share/nginx/html/nginx<span class="literal">-test</span>/all<span class="operator">-match</span>/;</span><br><span class="line">location / &#123;</span><br><span class="line">index index.html;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>3、访问分析</p><ul><li>当访问 <a href="http://www.nginx-test.com">www.nginx-test.com</a> 时，都可以被匹配上，因此选择优先级最高的“完全匹配”；</li><li>当访问 <a href="http://mail.nginx-test.com">mail.nginx-test.com</a> 时，会进行“左匹配”；</li><li>当访问 <a href="http://www.nginx-test.org">www.nginx-test.org</a> 时，会进行“右匹配”；</li><li>当访问 <a href="http://doc.nginx-test.com">doc.nginx-test.com</a> 时，会进行“左匹配”；</li><li>当访问 <a href="http://www.nginx-test.cn">www.nginx-test.cn</a> 时，会进行“右匹配”；</li><li>当访问 fe.nginx-test.club 时，会进行“正则匹配”；</li></ul><h3 id="root">root</h3><p>指定静态资源目录位置，它可以写在 http 、 server 、 location 等配置中。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">root path</span><br><span class="line"></span><br><span class="line">例如：</span><br><span class="line">location /image &#123;</span><br><span class="line">root /opt/nginx/<span class="keyword">static</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">当用户访问 www.test.com/image/<span class="number">1</span>.png 时，实际在服务器找的路径是 /opt/nginx/<span class="keyword">static</span>/image/<span class="number">1</span>.png</span><br></pre></td></tr></table></figure><p>[注意] root 会将定义路径与 URI 叠加， alias 则只取定义路径。</p><h3 id="alias">alias</h3><p>它也是指定静态资源目录位置，它只能写在 location 中。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">location /image &#123;</span><br><span class="line">alias /opt/nginx/<span class="keyword">static</span>/image/;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">当用户访问 www.test.com/image/<span class="number">1</span>.png 时，实际在服务器找的路径是 /opt/nginx/<span class="keyword">static</span>/image/<span class="number">1</span>.png</span><br></pre></td></tr></table></figure><p>[注意] 使用 alias 末尾一定要添加 / ，并且它只能位于 location 中。</p><h3 id="location">location</h3><p>配置路径。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">location [ = | ~ | ~* | ^~ ] uri &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>匹配规则：</p><ul><li>= 精确匹配；</li><li>~ 正则匹配，区分大小写；</li><li>~* 正则匹配，不区分大小写；</li><li>^~ 匹配到即停止搜索；</li></ul><p>匹配优先级： <strong>= &gt; ^~ &gt; ~ &gt; ~* &gt; 不带任何字符。</strong><br>实例：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen<span class="number">80</span>;</span><br><span class="line">  server_namewww.nginx<span class="literal">-test</span>.com;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 只有当访问 www.nginx-test.com/match_all/ 时才会匹配到/usr/share/nginx/html/match_all/index.html</span></span><br><span class="line">  location = /match_all/ &#123;</span><br><span class="line">      root/usr/share/nginx/html</span><br><span class="line">      index index.html</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 当访问 www.nginx-test.com/1.jpg 等路径时会去 /usr/share/nginx/images/1.jpg 找对应的资源</span></span><br><span class="line">  location ~ \.(jpeg|jpg|png|svg)<span class="variable">$</span> &#123;</span><br><span class="line">  root /usr/share/nginx/images;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment"># 当访问 www.nginx-test.com/bbs/ 时会匹配上 /usr/share/nginx/html/bbs/index.html</span></span><br><span class="line">  location ^~ /bbs/ &#123;</span><br><span class="line">  root /usr/share/nginx/html;</span><br><span class="line">    index index.html index.htm;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="location-中的反斜线">location 中的反斜线</h4><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">location /test &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">location /test/ &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>不带 / 当访问 <a href="http://www.nginx-test.com/test">www.nginx-test.com/test</a> 时， Nginx 先找是否有 test 目录，如果有则找 test<br>目录下的 index.html ；如果没有 test 目录， nginx 则会找是否有 test 文件。</li><li>带 / 当访问 <a href="http://www.nginx-test.com/test">www.nginx-test.com/test</a> 时， Nginx 先找是否有 test 目录，如果有则找 test<br>目录下的 index.html ，如果没有它也不会去找是否存在 test 文件。</li></ul><h4 id="return">return</h4><p>停止处理请求，直接返回响应码或重定向到其他 URL ；执行 return 指令后， location 中后续指令将不会被执行。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> code [<span class="type">text</span>];</span><br><span class="line"><span class="keyword">return</span> code URL;</span><br><span class="line"><span class="keyword">return</span> URL;</span><br><span class="line"></span><br><span class="line">例如：</span><br><span class="line">location / &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">404</span>; <span class="comment"># 直接返回状态码</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">location / &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">404</span> <span class="string">&quot;pages not found&quot;</span>; <span class="comment"># 返回状态码 + 一段文本</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">location / &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">302</span> /bbs ; <span class="comment"># 返回状态码 + 重定向地址</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">location / &#123;</span><br><span class="line"><span class="keyword">return</span> https://www.baidu.com ; <span class="comment"># 返回重定向地址</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="rewrite">rewrite</h3><p>根据指定正则表达式匹配规则，重写 URL 。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：rewrite 正则表达式 要替换的内容 [<span class="type">flag</span>];</span><br><span class="line"></span><br><span class="line">上下文：server、location、<span class="keyword">if</span></span><br><span class="line"></span><br><span class="line">示例：rewirte /images/(.*\.jpg)<span class="variable">$</span> /pic/<span class="variable">$1</span>; <span class="comment"># $1是前面括号(.*\.jpg)的反向引用</span></span><br></pre></td></tr></table></figure><p>flag 可选值的含义：</p><ul><li>last 重写后的 URL 发起新请求，再次进入 server 段，重试 location 的中的匹配；</li><li>break 直接使用重写后的 URL ，不再匹配其它 location 中语句；</li><li>redirect 返回302临时重定向；</li><li>permanent 返回301永久重定向；</li></ul><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">server&#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name fe.lion.club; <span class="comment"># 要在本地hosts文件进行配置</span></span><br><span class="line">  root html;</span><br><span class="line">  location /search &#123;</span><br><span class="line">  rewrite ^/(.*) https://www.baidu.com redirect;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  location /images &#123;</span><br><span class="line">  rewrite /images/(.*) /pics/<span class="variable">$1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  location /pics &#123;</span><br><span class="line">  rewrite /pics/(.*) /photos/<span class="variable">$1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  location /photos &#123;</span><br><span class="line">  </span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>按照这个配置我们来分析：</p><ul><li>当访问 fe.lion.club/search 时，会自动帮我们重定向到 <a href="https://www.baidu.com">https://www.baidu.com</a>。</li><li>当访问 fe.lion.club/images/1.jpg 时，第一步重写 URL 为 fe.lion.club/pics/1.jpg<br>，找到 pics 的 location ，继续重写 URL 为 fe.lion.club/photos/1.jpg ，找到 /photos<br>的 location 后，去 html/photos 目录下寻找 1.jpg 静态资源。</li></ul><h3 id="if-指令">if 指令</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">语法：<span class="keyword">if</span> (condition) &#123;...&#125;</span><br><span class="line"></span><br><span class="line">上下文：server、location</span><br><span class="line"></span><br><span class="line">示例：</span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$http_user_agent</span> ~ Chrome)&#123;</span><br><span class="line">  rewrite /(.*)/browser/<span class="variable">$1</span> <span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>condition 判断条件：</p><ul><li>$variable 仅为变量时，值为空或以0开头字符串都会被当做 false 处理；</li><li>= 或 != 相等或不等；</li><li>~ 正则匹配；</li><li>! ~ 非正则匹配；</li><li>~* 正则匹配，不区分大小写；</li><li>-f 或 ! -f 检测文件存在或不存在；</li><li>-d 或 ! -d 检测目录存在或不存在；</li><li>-e 或 ! -e 检测文件、目录、符号链接等存在或不存在；</li><li>-x 或 ! -x 检测文件可以执行或不可执行；</li></ul><p>实例：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">8080</span>;</span><br><span class="line">  server_name localhost;</span><br><span class="line">  root html;</span><br><span class="line">  </span><br><span class="line">  location / &#123;</span><br><span class="line">  <span class="keyword">if</span> ( <span class="variable">$uri</span> = <span class="string">&quot;/images/&quot;</span> )&#123;</span><br><span class="line">    rewrite (.*) /pics/ <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当访问 localhost:8080/images/ 时，会进入 if 判断里面执行 rewrite 命令。</p><h3 id="autoindex">autoindex</h3><p>用户请求以 / 结尾时，列出目录结构，可以用于快速搭建静态资源下载网站。<br>autoindex.conf 配置信息：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name fe.lion<span class="literal">-test</span>.club;</span><br><span class="line">  </span><br><span class="line">  location /download/ &#123;</span><br><span class="line">    root /opt/source;</span><br><span class="line">    </span><br><span class="line">    autoindex on; <span class="comment"># 打开 autoindex，，可选参数有 on | off</span></span><br><span class="line">    autoindex_exact_size on; <span class="comment"># 修改为off，以KB、MB、GB显示文件大小，默认为on，以bytes显示出⽂件的确切⼤⼩</span></span><br><span class="line">    autoindex_format html; <span class="comment"># 以html的方式进行格式化，可选参数有 html | json | xml</span></span><br><span class="line">    autoindex_localtime off; <span class="comment"># 显示的⽂件时间为⽂件的服务器时间。默认为off，显示的⽂件时间为GMT时间</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当访问 <a href="http://fe.lion.com/download/">fe.lion.com/download/</a> 时，会把服务器 /opt/source/download/ 路径下的文件展示出来，如下图所示：<br><img src="https://img-blog.csdnimg.cn/202105080940050.png" alt="在这里插入图片描述"></p><h3 id="变量">变量</h3><p>Nginx 提供给使用者的变量非常多，但是终究是一个完整的请求过程所产生数据， Nginx 将这些数据以变量的形式提供给使用者。</p><p>下面列举些项目中常用的变量：</p><table><thead><tr><th>变量名</th><th>含义</th></tr></thead><tbody><tr><td>remote_addr</td><td>客户端 IP 地址</td></tr><tr><td>remote_port</td><td>客户端端口</td></tr><tr><td>server_addr</td><td>服务端 IP 地址</td></tr><tr><td>server_port</td><td>服务端端口</td></tr><tr><td>server_protocol</td><td>服务端协议</td></tr><tr><td>binary_remote_addr</td><td>二进制格式的客户端 IP 地址</td></tr><tr><td>connection TCP</td><td>连接的序号，递增</td></tr><tr><td>connection_request TCP</td><td>连接当前的请求数量uri 请求的URL，不包含参数</td></tr><tr><td>request_uri</td><td>请求的URL，包含参数scheme 协议名， http 或 https request_method 请求方法</td></tr><tr><td>request_length</td><td>全部请求的长度，包含请求行、请求头、请求体args 全部参数字符串arg_参数名 获取特定参数值</td></tr><tr><td>is_args URL</td><td>中是否有参数，有的话返回 ? ，否则返回空</td></tr><tr><td>query_string</td><td>与 args 相同</td></tr><tr><td>host</td><td>请求信息中的 Host ，如果请求中没有 Host 行，则在请求头中找，最后使用 nginx 中设置的 server_name 。</td></tr><tr><td>http_user_agent</td><td>用户浏览器</td></tr><tr><td>http_referer</td><td>从哪些链接过来的请求</td></tr><tr><td>http_via</td><td>每经过一层代理服务器，都会添加相应的信息</td></tr><tr><td>http_cookie</td><td>获取用户 cookie</td></tr><tr><td>request_time</td><td>处理请求已消耗的时间</td></tr><tr><td>https</td><td>是否开启了 https ，是则返回 on ，否则返回空</td></tr><tr><td>request_filename</td><td>磁盘文件系统待访问文件的完整路径</td></tr><tr><td>document_root</td><td>由 URI 和 root/alias 规则生成的文件夹路径</td></tr><tr><td>limit_rate</td><td>返回响应时的速度上限值</td></tr></tbody></table><p>实例演示 var.conf ：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">server&#123;</span><br><span class="line">listen <span class="number">8081</span>;</span><br><span class="line">server_name var.lion<span class="literal">-test</span>.club;</span><br><span class="line">root /usr/share/nginx/html;</span><br><span class="line">location / &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">200</span> <span class="string">&quot;</span></span><br><span class="line"><span class="string">remote_addr: <span class="variable">$remote_addr</span></span></span><br><span class="line"><span class="string">remote_port: <span class="variable">$remote_port</span></span></span><br><span class="line"><span class="string">server_addr: <span class="variable">$server_addr</span></span></span><br><span class="line"><span class="string">server_port: <span class="variable">$server_port</span></span></span><br><span class="line"><span class="string">server_protocol: <span class="variable">$server_protocol</span></span></span><br><span class="line"><span class="string">binary_remote_addr: <span class="variable">$binary_remote_addr</span></span></span><br><span class="line"><span class="string">connection: <span class="variable">$connection</span></span></span><br><span class="line"><span class="string">uri: <span class="variable">$uri</span></span></span><br><span class="line"><span class="string">request_uri: <span class="variable">$request_uri</span></span></span><br><span class="line"><span class="string">scheme: <span class="variable">$scheme</span></span></span><br><span class="line"><span class="string">request_method: <span class="variable">$request_method</span></span></span><br><span class="line"><span class="string">request_length: <span class="variable">$request_length</span></span></span><br><span class="line"><span class="string">args: <span class="variable">$args</span></span></span><br><span class="line"><span class="string">arg_pid: <span class="variable">$arg_pid</span></span></span><br><span class="line"><span class="string">is_args: <span class="variable">$is_args</span></span></span><br><span class="line"><span class="string">query_string: <span class="variable">$query_string</span></span></span><br><span class="line"><span class="string">host: <span class="variable">$host</span></span></span><br><span class="line"><span class="string">http_user_agent: <span class="variable">$http_user_agent</span></span></span><br><span class="line"><span class="string">http_referer: <span class="variable">$http_referer</span></span></span><br><span class="line"><span class="string">http_via: <span class="variable">$http_via</span></span></span><br><span class="line"><span class="string">request_time: <span class="variable">$request_time</span></span></span><br><span class="line"><span class="string">https: <span class="variable">$https</span></span></span><br><span class="line"><span class="string">request_filename: <span class="variable">$request_filename</span></span></span><br><span class="line"><span class="string">document_root: <span class="variable">$document_root</span></span></span><br><span class="line"><span class="string">&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我们访问 <a href="http://var.lion-test.club:8081/test?pid=121414&amp;cid=sadasd">http://var.lion-test.club:8081/test?pid=121414&amp;cid=sadasd</a> 时，由于 Nginx 中写了 return 方法，因此 chrome 浏览器会默认为我们下载一个文件，下面展示的就是下载的文件内容：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">remote_addr: <span class="number">27.16</span>.<span class="number">220.84</span></span><br><span class="line">remote_port: <span class="number">56838</span></span><br><span class="line">server_addr: <span class="number">172.17</span>.<span class="number">0.2</span></span><br><span class="line">server_port: <span class="number">8081</span></span><br><span class="line">server_protocol: HTTP/<span class="number">1.1</span></span><br><span class="line">binary_remote_addr: 茉</span><br><span class="line">connection: <span class="number">126</span></span><br><span class="line">uri: /test/</span><br><span class="line">request_uri: /test/?pid=<span class="number">121414</span>&amp;cid=sadasd</span><br><span class="line">scheme: http</span><br><span class="line">request_method: GET</span><br><span class="line">request_length: <span class="number">518</span></span><br><span class="line">args: pid=<span class="number">121414</span>&amp;cid=sadasd</span><br><span class="line">arg_pid: <span class="number">121414</span></span><br><span class="line">is_args: ?</span><br><span class="line">query_string: pid=<span class="number">121414</span>&amp;cid=sadasd</span><br><span class="line">host: var.lion<span class="literal">-test</span>.club</span><br><span class="line">http_user_agent: Mozilla/<span class="number">5.0</span> (Macintosh; Intel Mac OS X <span class="number">10</span>_14_0) AppleWebKit/<span class="number">537.36</span> (KHTML, like Gecko) Chrome/<span class="number">88.0</span>.<span class="number">4324.182</span> Safari/<span class="number">537.36</span></span><br><span class="line">http_referer: </span><br><span class="line">http_via: </span><br><span class="line">request_time: <span class="number">0.000</span></span><br><span class="line">https: </span><br><span class="line">request_filename: /usr/share/nginx/html/test/</span><br><span class="line">document_root: /usr/share/nginx/html</span><br></pre></td></tr></table></figure><p>Nginx 的配置还有非常多，以上只是罗列了一些常用的配置，在实际项目中还是要学会查阅文档。</p><h2 id="Nginx-应用核心概念">Nginx 应用核心概念</h2><p>代理是在服务器和客户端之间假设的一层服务器，代理将接收客户端的请求并将它转发给服务器，然后将服务端的响应转发给客户端。</p><p>不管是正向代理还是反向代理，实现的都是上面的功能。</p><h3 id="正向代理">正向代理</h3><blockquote><p>正向代理，意思是一个位于客户端和原始服务器(origin server)之间的服务器，为了从原始服务器取得内容，客户端向代理发送一个请求并指定目标(原始服务器)，然后代理向原始服务器转交请求并将获得的内容返回给客户端。</p></blockquote><p>正向代理是为我们服务的，即为客户端服务的，客户端可以根据正向代理访问到它本身无法访问到的服务器资源。<br>正向代理对我们是透明的，对服务端是非透明的，即服务端并不知道自己收到的是来自代理的访问还是来自真实客户端的访问。</p><h3 id="反向代理">反向代理</h3><blockquote><p>反向代理*（Reverse Proxy）方式是指以代理服务器来接受internet上的连接请求，然后将请求转发给内部网络上的服务器，并将从服务器上得到的结果返回给internet上请求连接的客户端，此时代理服务器对外就表现为一个反向代理服务器。</p></blockquote><p>反向代理是为服务端服务的，反向代理可以帮助服务器接收来自客户端的请求，帮助服务器做请求转发，负载均衡等。<br>反向代理对服务端是透明的，对我们是非透明的，即我们并不知道自己访问的是代理服务器，而服务器知道反向代理在为他服务。<br>反向代理的优势：</p><ul><li>隐藏真实服务器；</li><li>负载均衡便于横向扩充后端动态服务；</li><li>动静分离，提升系统健壮性；</li></ul><p>那么“动静分离”是什么？负载均衡又是什么？</p><h3 id="动静分离">动静分离</h3><p>动静分离是指在 web 服务器架构中，将静态页面与动态页面或者静态内容接口和动态内容接口分开不同系统访问的架构设计方法，进而提示整个服务的访问性和可维护性。<br><img src="https://img-blog.csdnimg.cn/20210508094850198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>一般来说，都需要将动态资源和静态资源分开，由于 Nginx 的高并发和静态资源缓存等特性，经常将静态资源部署在 Nginx 上。如果请求的是静态资源，直接到静态资源目录获取资源，如果是动态资源的请求，则利用反向代理的原理，把请求转发给对应后台应用去处理，从而实现动静分离。</p><p>使用前后端分离后，可以很大程度提升静态资源的访问速度，即使动态服务不可用，静态资源的访问也不会受到影响。</p><h3 id="负载均衡">负载均衡</h3><p>一般情况下，客户端发送多个请求到服务器，服务器处理请求，其中一部分可能要操作一些资源比如数据库、静态资源等，服务器处理完毕后，再将结果返回给客户端。</p><p>这种模式对于早期的系统来说，功能要求不复杂，且并发请求相对较少的情况下还能胜任，成本也低。随着信息数量不断增长，访问量和数据量飞速增长，以及系统业务复杂度持续增加，这种做法已无法满足要求，并发量特别大时，服务器容易崩。</p><p>很明显这是由于服务器性能的瓶颈造成的问题，除了堆机器之外，最重要的做法就是负载均衡。</p><p>请求爆发式增长的情况下，单个机器性能再强劲也无法满足要求了，这个时候集群的概念产生了，单个服务器解决不了的问题，可以使用多个服务器，然后将请求分发到各个服务器上，将负载分发到不同的服务器，这就是负载均衡，核心是「分摊压力」。 Nginx 实现负载均衡，一般来说指的是将请求转发给服务器集群。</p><p>举个具体的例子，晚高峰乘坐地铁的时候，入站口经常会有地铁工作人员大喇叭“请走 B 口， B 口人少车空…”，这个工作人员的作用就是负载均衡。<br><img src="https://img-blog.csdnimg.cn/20210508094906537.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>Nginx 实现负载均衡的策略：</p><ul><li>轮询策略：默认情况下采用的策略，将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的，但是如果其中某一台服务器压力太大，出现延迟，会影响所有分配在这台服务器下的用户。</li><li>最小连接数策略：将请求优先分配给压力较小的服务器，它可以平衡每个队列的长度，并避免向压力大的服务器添加更多的请求。</li><li>最快响应时间策略：优先分配给响应时间最短的服务器。</li><li>客户端 ip 绑定策略：来自同一个 ip 的请求永远只分配一台服务器，有效解决了动态网页存在的 session 共享问题。</li></ul><h2 id="Nginx-实战配置">Nginx 实战配置</h2><p>在配置反向代理和负载均衡等等功能之前，有两个核心模块是我们必须要掌握的，这两个模块应该说是 Nginx 应用配置中的核心，它们分别是： upstream 、proxy_pass 。</p><h3 id="upstream">upstream</h3><p>用于定义上游服务器（指的就是后台提供的应用服务器）的相关信息。<br><img src="https://img-blog.csdnimg.cn/2021050809492298.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">语法：upstream name &#123;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">上下文：http</span><br><span class="line"></span><br><span class="line">示例：</span><br><span class="line">upstream back_end_server&#123;</span><br><span class="line">  server <span class="number">192.168</span>.<span class="number">100.33</span>:<span class="number">8081</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 upstream 内可使用的指令：</p><ul><li>server 定义上游服务器地址；</li><li>zone 定义共享内存，用于跨 worker 子进程；</li><li>keepalive 对上游服务启用长连接；</li><li>keepalive_requests 一个长连接最多请求 HTTP 的个数；</li><li>keepalive_timeout 空闲情形下，一个长连接的超时时长；</li><li>hash 哈希负载均衡算法；</li><li>ip_hash 依据 IP 进行哈希计算的负载均衡算法；</li><li>least_conn 最少连接数负载均衡算法；</li><li>least_time 最短响应时间负载均衡算法；</li><li>random 随机负载均衡算法；</li></ul><h3 id="server">server</h3><p>定义上游服务器地址。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">语法：server address [<span class="type">parameters</span>]</span><br><span class="line"></span><br><span class="line">上下文：upstream</span><br></pre></td></tr></table></figure><p>parameters 可选值：</p><ul><li>weight=number 权重值，默认为1；</li><li>max_conns=number 上游服务器的最大并发连接数；</li><li>fail_timeout=time 服务器不可用的判定时间；</li><li>max_fails=numer 服务器不可用的检查次数；</li><li>backup 备份服务器，仅当其他服务器都不可用时才会启用；</li><li>down 标记服务器长期不可用，离线维护；</li></ul><h3 id="keepalive">keepalive</h3><p>限制每个 worker 子进程与上游服务器空闲长连接的最大数量。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">keepalive connections;</span><br><span class="line"></span><br><span class="line">上下文：upstream</span><br><span class="line"></span><br><span class="line">示例：keepalive <span class="number">16</span>;</span><br></pre></td></tr></table></figure><h3 id="keepalive-requests">keepalive_requests</h3><p>单个长连接可以处理的最多 HTTP 请求个数。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：keepalive_requests number;</span><br><span class="line"></span><br><span class="line">默认值：keepalive_requests <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line">上下文：upstream</span><br></pre></td></tr></table></figure><h3 id="keepalive-timeout">keepalive_timeout</h3><p>空闲长连接的最长保持时间。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：keepalive_timeout time;</span><br><span class="line"></span><br><span class="line">默认值：keepalive_timeout <span class="number">60</span>s;</span><br><span class="line"></span><br><span class="line">上下文：upstream</span><br></pre></td></tr></table></figure><h3 id="配置实例">配置实例</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">upstream back_end&#123;</span><br><span class="line">server <span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8081</span> weight=<span class="number">3</span> max_conns=<span class="number">1000</span> fail_timeout=<span class="number">10</span>s max_fails=<span class="number">2</span>;</span><br><span class="line">  keepalive <span class="number">32</span>;</span><br><span class="line">  keepalive_requests <span class="number">50</span>;</span><br><span class="line">  keepalive_timeout <span class="number">30</span>s;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="proxy-pass">proxy_pass</h3><p>用于配置代理服务器。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_pass URL;</span><br><span class="line"></span><br><span class="line">上下文：location、<span class="keyword">if</span>、limit_except</span><br><span class="line"></span><br><span class="line">示例：</span><br><span class="line">proxy_pass http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8081</span></span><br><span class="line">proxy_pass http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8081</span>/proxy</span><br></pre></td></tr></table></figure><p>URL 参数原则</p><ul><li>URL 必须以 http 或 https 开头；</li><li>URL 中可以携带变量；</li><li>URL 中是否带 URI ，会直接影响发往上游请求的 URL ；</li></ul><p>接下来让我们来看看两种常见的 URL 用法：</p><ul><li>proxy_pass <a href="http://192.168.100.33:8081">http://192.168.100.33:8081</a></li><li>proxy_pass <a href="http://192.168.100.33:8081/">http://192.168.100.33:8081/</a></li></ul><p>这两种用法的区别就是带 / 和不带 / ，在配置代理时它们的区别可大了：</p><ul><li>不带 / 意味着 Nginx 不会修改用户 URL ，而是直接透传给上游的应用服务器；</li><li>带 / 意味着 Nginx 会修改用户 URL ，修改方法是将 location 后的 URL 从用户 URL 中删除；</li></ul><p>不带 / 的用法：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">location /bbs/&#123;</span><br><span class="line">  proxy_pass http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8080</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>分析：</p><ol><li>用户请求 URL ： /bbs/abc/test.html</li><li>请求到达 Nginx 的 URL ： /bbs/abc/test.html</li><li>请求到达上游应用服务器的 URL ： /bbs/abc/test.html</li></ol><p>带 / 的用法：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">location /bbs/&#123;</span><br><span class="line">  proxy_pass http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8080</span>/;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>分析：</p><ol><li>用户请求 URL ： /bbs/abc/test.html</li><li>请求到达 Nginx 的 URL ： /bbs/abc/test.html</li><li>请求到达上游应用服务器的 URL ： /abc/test.html</li></ol><p>并没有拼接上 /bbs ，这点和 root 与 alias 之间的区别是保持一致的。</p><h3 id="配置反向代理">配置反向代理</h3><p>这里为了演示更加接近实际，作者准备了两台云服务器，它们的公网 IP 分别是： 121.42.11.34 与 121.5.180.193 。<br>我们把 121.42.11.34 服务器作为上游服务器，做如下配置：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/nginx/conf.d/proxy.conf</span></span><br><span class="line">server&#123;</span><br><span class="line">  listen <span class="number">8080</span>;</span><br><span class="line">  server_name localhost;</span><br><span class="line">  </span><br><span class="line">  location /proxy/ &#123;</span><br><span class="line">    root /usr/share/nginx/html/proxy;</span><br><span class="line">    index index.html;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># /usr/share/nginx/html/proxy/index.html</span></span><br><span class="line">&lt;h1&gt; <span class="number">121.42</span>.<span class="number">11.34</span> proxy html &lt;/h1&gt;</span><br></pre></td></tr></table></figure><p>配置完成后重启 Nginx 服务器 nginx -s reload 。<br>把 121.5.180.193 服务器作为代理服务器，做如下配置：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/nginx/conf.d/proxy.conf</span></span><br><span class="line">upstream back_end &#123;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8080</span> weight=<span class="number">2</span> max_conns=<span class="number">1000</span> fail_timeout=<span class="number">10</span>s max_fails=<span class="number">3</span>;</span><br><span class="line">  keepalive <span class="number">32</span>;</span><br><span class="line">  keepalive_requests <span class="number">80</span>;</span><br><span class="line">  keepalive_timeout <span class="number">20</span>s;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name proxy.lion.club;</span><br><span class="line">  location /proxy &#123;</span><br><span class="line">  proxy_pass http://back_end/proxy;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本地机器要访问 proxy.lion.club 域名，因此需要配置本地 hosts ，通过命令：vim /etc/hosts 进入配置文件，添加如下内容：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">121.5</span>.<span class="number">180.193</span> proxy.lion.club</span><br></pre></td></tr></table></figure><p><img src="https://img-blog.csdnimg.cn/20210508101754614.png" alt="在这里插入图片描述"></p><p>分析：</p><ol><li>当访问 proxy.lion.club/proxy 时通过 upstream 的配置找到 121.42.11.34:8080 ；</li><li>因此访问地址变为 <a href="http://121.42.11.34:8080/proxy">http://121.42.11.34:8080/proxy</a> ；</li><li>连接到 121.42.11.34 服务器，找到 8080 端口提供的 server ；</li><li>通过 server 找到 /usr/share/nginx/html/proxy/index.html 资源，最终展示出来。</li></ol><h3 id="配置负载均衡">配置负载均衡</h3><p>配置负载均衡主要是要使用 upstream 指令。<br>我们把 121.42.11.34 服务器作为上游服务器，做如下配置（ /etc/nginx/conf.d/balance.conf ）：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">server&#123;</span><br><span class="line">  listen <span class="number">8020</span>;</span><br><span class="line">  location / &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">200</span> <span class="string">&#x27;return 8020 \n&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server&#123;</span><br><span class="line">  listen <span class="number">8030</span>;</span><br><span class="line">  location / &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">200</span> <span class="string">&#x27;return 8030 \n&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server&#123;</span><br><span class="line">  listen <span class="number">8040</span>;</span><br><span class="line">  location / &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">200</span> <span class="string">&#x27;return 8040 \n&#x27;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置完成后：</p><ol><li>nginx -t 检测配置是否正确；</li><li>nginx -s reload 重启 Nginx 服务器；</li><li>执行 ss -nlt 命令查看端口是否被占用，从而判断 Nginx 服务是否正确启动。</li></ol><p>把 121.5.180.193 服务器作为代理服务器，做如下配置（ /etc/nginx/conf.d/balance.conf ）：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">upstream demo_server &#123;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8020</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8030</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8040</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name balance.lion.club;</span><br><span class="line">  </span><br><span class="line">  location /balance/ &#123;</span><br><span class="line">  proxy_pass http://demo_server;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置完成后重启 Nginx 服务器。并且在需要访问的客户端配置好 ip 和域名的映射关系。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/hosts</span></span><br><span class="line"><span class="number">121.5</span>.<span class="number">180.193</span> balance.lion.club</span><br></pre></td></tr></table></figure><p>在客户端机器执行 curl <a href="http://balance.lion.club/balance/">http://balance.lion.club/balance/</a> 命令：<br><img src="https://img-blog.csdnimg.cn/20210508095050782.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>不难看出，负载均衡的配置已经生效了，每次给我们分发的上游服务器都不一样。就是通过简单的轮询策略进行上游服务器分发。</p><p>接下来，我们再来了解下 Nginx 的其它分发策略。</p><h4 id="hash-算法">hash 算法</h4><p>通过制定关键字作为 hash key ，基于 hash 算法映射到特定的上游服务器中。关键字可以包含有变量、字符串。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">upstream demo_server &#123;</span><br><span class="line">  hash <span class="variable">$request_uri</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8020</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8030</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8040</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name balance.lion.club;</span><br><span class="line">  </span><br><span class="line">  location /balance/ &#123;</span><br><span class="line">  proxy_pass http://demo_server;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>hash $request_uri 表示使用 request_uri 变量作为 hash 的 key 值，只要访问的 URI 保持不变，就会一直分发给同一台服务器。</p><h4 id="ip-hash">ip_hash</h4><p>根据客户端的请求 ip 进行判断，只要 ip 地址不变就永远分配到同一台主机。它可以有效解决后台服务器 session 保持的问题。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">upstream demo_server &#123;</span><br><span class="line">  ip_hash;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8020</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8030</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8040</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name balance.lion.club;</span><br><span class="line">  </span><br><span class="line">  location /balance/ &#123;</span><br><span class="line">  proxy_pass http://demo_server;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="最少连接数算法">最少连接数算法</h4><p>各个 worker 子进程通过读取共享内存的数据，来获取后端服务器的信息。来挑选一台当前已建立连接数最少的服务器进行分配请求。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">语法：least_conn;</span><br><span class="line"></span><br><span class="line">上下文：upstream;</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">upstream demo_server &#123;</span><br><span class="line">  zone test <span class="number">10</span>M; <span class="comment"># zone可以设置共享内存空间的名字和大小</span></span><br><span class="line">  least_conn;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8020</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8030</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">8040</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name balance.lion.club;</span><br><span class="line">  </span><br><span class="line">  location /balance/ &#123;</span><br><span class="line">  proxy_pass http://demo_server;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后你会发现，负载均衡的配置其实一点都不复杂。</p><h3 id="配置缓存">配置缓存</h3><p>缓存可以非常有效的提升性能，因此不论是客户端（浏览器），还是代理服务器（ Nginx ），乃至上游服务器都多少会涉及到缓存。可见缓存在每个环节都是非常重要的。下面让我们来学习 Nginx 中如何设置缓存策略。</p><h4 id="proxy-cache">proxy_cache</h4><p>存储一些之前被访问过、而且可能将要被再次访问的资源，使用户可以直接从代理服务器获得，从而减少上游服务器的压力，加快整个访问速度。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_cache zone | off ; <span class="comment"># zone 是共享内存的名称</span></span><br><span class="line"></span><br><span class="line">默认值：proxy_cache off;</span><br><span class="line"></span><br><span class="line">上下文：http、server、location</span><br></pre></td></tr></table></figure><h4 id="proxy-cache-path">proxy_cache_path</h4><p>设置缓存文件的存放路径。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_cache_path path [<span class="type">level</span>=<span class="type">levels</span>] ...可选参数省略，下面会详细列举</span><br><span class="line"></span><br><span class="line">默认值：proxy_cache_path off</span><br><span class="line"></span><br><span class="line">上下文：http</span><br></pre></td></tr></table></figure><p>参数含义：</p><ul><li>path 缓存文件的存放路径；</li><li>level path 的目录层级；</li><li>keys_zone 设置共享内存；</li><li>inactive 在指定时间内没有被访问，缓存会被清理，默认10分钟；</li></ul><h4 id="proxy-cache-key">proxy_cache_key</h4><p>设置缓存文件的 key 。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_cache_key</span><br><span class="line"></span><br><span class="line">默认值：proxy_cache_key <span class="variable">$scheme</span><span class="variable">$proxy_host</span><span class="variable">$request_uri</span>;</span><br><span class="line"></span><br><span class="line">上下文：http、server、location</span><br></pre></td></tr></table></figure><h4 id="proxy-cache-valid">proxy_cache_valid</h4><p>配置什么状态码可以被缓存，以及缓存时长。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_cache_valid [<span class="type">code...</span>] time;</span><br><span class="line"></span><br><span class="line">上下文：http、server、location</span><br><span class="line"></span><br><span class="line">配置示例：proxy_cache_valid <span class="number">200</span> <span class="number">304</span> <span class="number">2</span>m;; <span class="comment"># 说明对于状态为200和304的缓存文件的缓存时间是2分钟</span></span><br></pre></td></tr></table></figure><h4 id="proxy-no-cache">proxy_no_cache</h4><p>定义相应保存到缓存的条件，如果字符串参数的至少一个值不为空且不等于“ 0”，则将不保存该响应到缓存。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_no_cache string;</span><br><span class="line"></span><br><span class="line">上下文：http、server、location</span><br><span class="line"></span><br><span class="line">示例：proxy_no_cache <span class="variable">$http_pragma</span>    <span class="variable">$http_authorization</span>;</span><br></pre></td></tr></table></figure><h4 id="proxy-cache-bypass">proxy_cache_bypass</h4><p>定义条件，在该条件下将不会从缓存中获取响应。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">语法：proxy_cache_bypass string;</span><br><span class="line"></span><br><span class="line">上下文：http、server、location</span><br><span class="line"></span><br><span class="line">示例：proxy_cache_bypass <span class="variable">$http_pragma</span>    <span class="variable">$http_authorization</span>;</span><br></pre></td></tr></table></figure><h4 id="upstream-cache-status-变量">upstream_cache_status 变量</h4><p>它存储了缓存是否命中的信息，会设置在响应头信息中，在调试中非常有用。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">MISS: 未命中缓存</span><br><span class="line">HIT： 命中缓存</span><br><span class="line">EXPIRED: 缓存过期</span><br><span class="line">STALE: 命中了陈旧缓存</span><br><span class="line">REVALIDDATED: Nginx验证陈旧缓存依然有效</span><br><span class="line">UPDATING: 内容陈旧，但正在更新</span><br><span class="line">BYPASS: X响应从原始服务器获取</span><br></pre></td></tr></table></figure><h4 id="配置实例-2">配置实例</h4><p>我们把 121.42.11.34 服务器作为上游服务器，做如下配置（ /etc/nginx/conf.d/cache.conf ）：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">1010</span>;</span><br><span class="line">  root /usr/share/nginx/html/<span class="number">1010</span>;</span><br><span class="line">  location / &#123;</span><br><span class="line">  index index.html;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">1020</span>;</span><br><span class="line">  root /usr/share/nginx/html/<span class="number">1020</span>;</span><br><span class="line">  location / &#123;</span><br><span class="line">  index index.html;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>把 121.5.180.193 服务器作为代理服务器，做如下配置（ /etc/nginx/conf.d/cache.conf ）：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">proxy_cache_path /etc/nginx/cache_temp levels=<span class="number">2</span>:<span class="number">2</span> keys_zone=cache_zone:<span class="number">30</span>m max_size=<span class="number">2</span>g inactive=<span class="number">60</span>m use_temp_path=off;</span><br><span class="line"></span><br><span class="line">upstream cache_server&#123;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">1010</span>;</span><br><span class="line">  server <span class="number">121.42</span>.<span class="number">11.34</span>:<span class="number">1020</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name cache.lion.club;</span><br><span class="line">  location / &#123;</span><br><span class="line">    proxy_cache cache_zone; <span class="comment"># 设置缓存内存，上面配置中已经定义好的</span></span><br><span class="line">    proxy_cache_valid <span class="number">200</span> <span class="number">5</span>m; <span class="comment"># 缓存状态为200的请求，缓存时长为5分钟</span></span><br><span class="line">    proxy_cache_key <span class="variable">$request_uri</span>; <span class="comment"># 缓存文件的key为请求的URI</span></span><br><span class="line">    add_header Nginx<span class="literal">-Cache-Status</span> <span class="variable">$upstream_cache_status</span> <span class="comment"># 把缓存状态设置为头部信息，响应给客户端</span></span><br><span class="line">    proxy_pass http://cache_server; <span class="comment"># 代理转发</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>缓存就是这样配置，我们可以在 /etc/nginx/cache_temp 路径下找到相应的缓存文件。</p><p>对于一些实时性要求非常高的页面或数据来说，就不应该去设置缓存，下面来看看如何配置不缓存的内容。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">80</span>;</span><br><span class="line">  server_name cache.lion.club;</span><br><span class="line">  <span class="comment"># URI 中后缀为 .txt 或 .text 的设置变量值为 &quot;no cache&quot;</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="variable">$request_uri</span> ~ \.(txt|text)<span class="variable">$</span>) &#123;</span><br><span class="line">  <span class="built_in">set</span> <span class="variable">$cache_name</span> <span class="string">&quot;no cache&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  location / &#123;</span><br><span class="line">    proxy_no_cache <span class="variable">$cache_name</span>; <span class="comment"># 判断该变量是否有值，如果有值则不进行缓存，如果没有值则进行缓存</span></span><br><span class="line">    proxy_cache cache_zone; <span class="comment"># 设置缓存内存</span></span><br><span class="line">    proxy_cache_valid <span class="number">200</span> <span class="number">5</span>m; <span class="comment"># 缓存状态为200的请求，缓存时长为5分钟</span></span><br><span class="line">    proxy_cache_key <span class="variable">$request_uri</span>; <span class="comment"># 缓存文件的key为请求的URI</span></span><br><span class="line">    add_header Nginx<span class="literal">-Cache-Status</span> <span class="variable">$upstream_cache_status</span> <span class="comment"># 把缓存状态设置为头部信息，响应给客户端</span></span><br><span class="line">    proxy_pass http://cache_server; <span class="comment"># 代理转发</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="HTTPS">HTTPS</h3><p>在学习如何配置 HTTPS 之前，我们先来简单回顾下 HTTPS 的工作流程是怎么样的？它是如何进行加密保证安全的？</p><h4 id="HTTPS-工作流程">HTTPS 工作流程</h4><ol><li>客户端（浏览器）访问 <a href="https://www.baidu.com">https://www.baidu.com</a> 百度网站；</li><li>百度服务器返回 HTTPS 使用的 CA 证书；</li><li>浏览器验证 CA 证书是否为合法证书；</li><li>验证通过，证书合法，生成一串随机数并使用公钥（证书中提供的）进行加密；</li><li>发送公钥加密后的随机数给百度服务器；</li><li>百度服务器拿到密文，通过私钥进行解密，获取到随机数（公钥加密，私钥解密，反之也可以）；</li><li>百度服务器把要发送给浏览器的内容，使用随机数进行加密后传输给浏览器；</li><li>此时浏览器可以使用随机数进行解密，获取到服务器的真实传输内容；</li></ol><p>这就是 HTTPS 的基本运作原理，使用对称加密和非对称机密配合使用，保证传输内容的安全性。<br><a href="https://juejin.cn/post/6844904148601667598#heading-37">关于HTTPS更多知识，可以查看作者的另外一篇文章《学习 HTTP 协议》</a>。</p><h4 id="配置证书">配置证书</h4><p>下载证书的压缩文件，里面有个 Nginx 文件夹，把 xxx.crt 和 xxx.key 文件拷贝到服务器目录，再进行如下配置：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">  listen <span class="number">443</span> ssl http2 default_server;   <span class="comment"># SSL 访问端口号为 443</span></span><br><span class="line">  server_name lion.club;         <span class="comment"># 填写绑定证书的域名(我这里是随便写的)</span></span><br><span class="line">  ssl_certificate /etc/nginx/https/lion.club_bundle.crt;   <span class="comment"># 证书地址</span></span><br><span class="line">  ssl_certificate_key /etc/nginx/https/lion.club.key;      <span class="comment"># 私钥地址</span></span><br><span class="line">  ssl_session_timeout <span class="number">10</span>m;</span><br><span class="line">  ssl_protocols TLSv1 TLSv1.<span class="number">1</span> TLSv1.<span class="number">2</span>; <span class="comment"># 支持ssl协议版本，默认为后三个，主流版本是[TLSv1.2]</span></span><br><span class="line"> </span><br><span class="line">  location / &#123;</span><br><span class="line">    root         /usr/share/nginx/html;</span><br><span class="line">    index        index.html index.htm;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如此配置后就能正常访问 HTTPS 版的网站了。</p><h3 id="配置跨域-CORS">配置跨域 CORS</h3><p>先简单回顾下跨域究竟是怎么回事。</p><h4 id="跨域的定义">跨域的定义</h4><p>同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。</p><h4 id="同源的定义">同源的定义</h4><p>如果两个页面的协议，端口（如果有指定）和域名都相同，则两个页面具有相同的源。<br>下面给出了与 URL <a href="http://store.company.com/dir/page.html">http://store.company.com/dir/page.html</a> 的源进行对比的示例:<br><a href="http://store.company.com/dir2/other.html">http://store.company.com/dir2/other.html</a> 同源</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">https://store.company.com/secure.html 不同源，协议不同</span><br><span class="line">http://store.company.com:<span class="number">81</span>/<span class="built_in">dir</span>/etc.html 不同源，端口不同</span><br><span class="line">http://news.company.com/<span class="built_in">dir</span>/other.html 不同源，主机不同</span><br></pre></td></tr></table></figure><p>不同源会有如下限制：</p><ul><li>Web 数据层面，同源策略限制了不同源的站点读取当前站点的 Cookie 、 IndexDB 、 LocalStorage 等数据。</li><li>DOM 层面，同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。</li><li>网络层面，同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。</li></ul><h4 id="Nginx-解决跨域的原理">Nginx 解决跨域的原理</h4><p>例如：</p><ul><li>前端 server 的域名为： <a href="http://fe.server.com">fe.server.com</a></li><li>后端服务的域名为： <a href="http://dev.server.com">dev.server.com</a></li></ul><p>现在我在 <a href="http://fe.server.com">fe.server.com</a> 对 <a href="http://dev.server.com">dev.server.com</a> 发起请求一定会出现跨域。<br>现在我们只需要启动一个 Nginx 服务器，将 server_name 设置为 <a href="http://fe.server.com">fe.server.com</a> 然后设置相应的 location 以拦截前端需要跨域的请求，最后将请求代理回 <a href="http://dev.server.com">dev.server.com</a> 。如下面的配置：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">server &#123;</span><br><span class="line">listen    <span class="number">80</span>;</span><br><span class="line">server_name  fe.server.com;</span><br><span class="line">location / &#123;</span><br><span class="line">proxy_pass dev.server.com;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样可以完美绕过浏览器的同源策略： <a href="http://fe.server.com">fe.server.com</a> 访问 Nginx 的 <a href="http://fe.server.com">fe.server.com</a> 属于同源访问，而 Nginx 对服务端转发的请求不会触发浏览器的同源策略。</p><h3 id="配置开启-gzip-压缩">配置开启 gzip 压缩</h3><p>GZIP 是规定的三种标准 HTTP 压缩格式之一。目前绝大多数的网站都在使用 GZIP 传输 HTML 、CSS 、 JavaScript 等资源文件。</p><p>对于文本文件， GZiP 的效果非常明显，开启后传输所需流量大约会降至 1/4~1/3 。</p><p>并不是每个浏览器都支持 gzip 的，如何知道客户端是否支持 gzip 呢，请求头中的 Accept-Encoding 来标识对压缩的支持。<br><img src="https://img-blog.csdnimg.cn/2021050809512035.png" alt="在这里插入图片描述"><br>启用 gzip 同时需要客户端和服务端的支持，如果客户端支持 gzip 的解析，那么只要服务端能够返回 gzip 的文件就可以启用 gzip 了,我们可以通过 Nginx 的配置来让服务端支持 gzip 。下面的 respone 中 content-encoding:gzip ，指服务端开启了 gzip 的压缩方式。<br><img src="https://img-blog.csdnimg.cn/20210508095129336.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>在 /etc/nginx/conf.d/ 文件夹中新建配置文件 gzip.conf ：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># # 默认off，是否开启gzip</span></span><br><span class="line">gzip on; </span><br><span class="line"><span class="comment"># 要采用 gzip 压缩的 MIME 文件类型，其中 text/html 被系统强制启用；</span></span><br><span class="line">gzip_types text/plain text/css application/json application/x<span class="literal">-javascript</span> text/xml application/xml application/xml+rss text/javascript;</span><br><span class="line"></span><br><span class="line"><span class="comment"># ---- 以上两个参数开启就可以支持Gzip压缩了 ---- #</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 默认 off，该模块启用后，Nginx 首先检查是否存在请求静态文件的 gz 结尾的文件，如果有则直接返回该 .gz 文件内容；</span></span><br><span class="line">gzip_static on;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 默认 off，nginx做为反向代理时启用，用于设置启用或禁用从代理服务器上收到相应内容 gzip 压缩；</span></span><br><span class="line">gzip_proxied any;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 用于在响应消息头中添加 Vary：Accept-Encoding，使代理服务器根据请求头中的 Accept-Encoding 识别是否启用 gzip 压缩；</span></span><br><span class="line">gzip_vary on;</span><br><span class="line"></span><br><span class="line"><span class="comment"># gzip 压缩比，压缩级别是 1-9，1 压缩级别最低，9 最高，级别越高压缩率越大，压缩时间越长，建议 4-6；</span></span><br><span class="line">gzip_comp_level <span class="number">6</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取多少内存用于缓存压缩结果，16 8k 表示以 8k*16 为单位获得；</span></span><br><span class="line">gzip_buffers <span class="number">16</span> <span class="number">8</span>k;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 允许压缩的页面最小字节数，页面字节数从header头中的 Content-Length 中进行获取。默认值是 0，不管页面多大都压缩。建议设置成大于 1k 的字节数，小于 1k 可能会越压越大；</span></span><br><span class="line"><span class="comment"># gzip_min_length 1k;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 默认 1.1，启用 gzip 所需的 HTTP 最低版本；</span></span><br><span class="line">gzip_http_version <span class="number">1.1</span>;</span><br></pre></td></tr></table></figure><p>其实也可以通过前端构建工具例如 webpack 、rollup 等在打生产包时就做好 Gzip 压缩，然后放到 Nginx 服务器中，这样可以减少服务器的开销，加快访问速度。</p><p>关于 Nginx 的实际应用就学习到这里，相信通过掌握了 Nginx 核心配置以及实战配置，之后再遇到什么需求，我们也能轻松应对。接下来，让我们再深入一点学习下 Nginx 的架构。</p><h2 id="Nginx-架构">Nginx 架构</h2><h3 id="进程结构">进程结构</h3><p>多进程结构 Nginx 的进程模型图：<br><img src="https://img-blog.csdnimg.cn/202105080951442.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"><br>多进程中的 Nginx 进程架构如下图所示，会有一个父进程（ Master Process ），它会有很多子进程（ Child Processes ）。</p><ul><li>Master Process 用来管理子进程的，其本身并不真正处理用户请求。<ul><li>某个子进程 down 掉的话，它会向 Master 进程发送一条消息，表明自己不可用了，此时 Master 进程会去新起一个子进程。</li><li>某个配置文件被修改了 Master 进程会去通知 work 进程获取新的配置信息，这也就是我们所说的热部署。</li></ul></li><li>子进程间是通过共享内存的方式进行通信的。</li></ul><h3 id="配置文件重载原理">配置文件重载原理</h3><p>reload 重载配置文件的流程：</p><ol><li>向 master 进程发送 HUP 信号（ reload 命令）；</li><li>master 进程检查配置语法是否正确；</li><li>master 进程打开监听端口；</li><li>master 进程使用新的配置文件启动新的 worker 子进程；</li><li>master 进程向老的 worker 子进程发送 QUIT 信号；</li><li>老的 worker 进程关闭监听句柄，处理完当前连接后关闭进程；</li><li>整个过程 Nginx 始终处于平稳运行中，实现了平滑升级，用户无感知；</li></ol><h3 id="Nginx-模块化管理机制">Nginx 模块化管理机制</h3><p>Nginx 的内部结构是由核心部分和一系列的功能模块所组成。这样划分是为了使得每个模块的功能相对简单，便于开发，同时也便于对系统进行功能扩展。Nginx 的模块是互相独立的,低耦合高内聚。<br><img src="https://img-blog.csdnimg.cn/20210508095203333.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hlbGxvV3VkZQ==,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;meta name=&quot;referrer&quot; content=&quot;no-referrer&quot; /&gt;
&lt;h2 id=&quot;Nginx-概述&quot;&gt;Nginx 概述&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://img-blog.csdnimg.cn/20210508090822890.pn</summary>
      
    
    
    
    <category term="linux" scheme="https://blog.mailjob.net/categories/linux/"/>
    
    
    <category term="linux" scheme="https://blog.mailjob.net/tags/linux/"/>
    
  </entry>
  
  <entry>
    <title>Jetbrains激活到2099年</title>
    <link href="https://blog.mailjob.net/posts/3196436062.html"/>
    <id>https://blog.mailjob.net/posts/3196436062.html</id>
    <published>2022-03-25T02:36:39.000Z</published>
    <updated>2026-07-25T09:01:29.895Z</updated>
    
    <content type="html"><![CDATA[<p>在 <a href="https://blog.mailjob.net/posts/2414163873.html">Jetbrains 产品无限重置方法</a> 我们讲了使用 <code>IDE Eval Reset</code> 插件激活 Jetbrains 产品的方法，但是由于该产品的作者在 <a href="https://gitee.com/pengzhile/ide-eval-resetter">gitee</a> 宣布该插件停止维护，所以笔者在继续使用该插件的时候，发现存在不稳定的情况，例如编辑器已经 Reset 但是还继续提示我激活。所以为了继续可以使用 Jetbrains 的产品，找到了另一种可以激活该产品的方法。</p><h2 id="激活步骤">激活步骤</h2><h3 id="插件激活">插件激活</h3><p>下载 <a href="https://gitee.com/code_soft/ja-netfilter">ja-netfilter</a> 插件，将改插件复制到编辑器的差距目录，我这里选择放置于我电脑的 PhpStorm 的插件目录 <code>/Applications/PhpStorm.app/Contents/plugins/ja-netfilter</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><span class="line">libin@bogon ja-netfilter % <span class="built_in">pwd</span></span><br><span class="line">/Applications/PhpStorm.app/Contents/plugins/ja-netfilter</span><br><span class="line"></span><br><span class="line">libin@bogon ja-netfilter % <span class="built_in">ls</span></span><br><span class="line">README.pdfjanf_config.txtsha1sum.txt ja-netfilter.jarplugins</span><br></pre></td></tr></table></figure><p>打开软件，进入 <code>help -&gt; edit VM options</code>加入一些代码</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-javaagent:/Applications/PhpStorm.app/Contents/plugins/ja-netfilter/ja-netfilter.jar</span><br></pre></td></tr></table></figure><p>注意：不可照抄，根据你放置破解插件的实际目录填写。<br><img src="http://img.github.mailjob.net/20220325114739.png" alt="image-20220325114736715"><br>完成后，重启软件，激活完成！</p><h3 id="试用期已过">试用期已过</h3><p>试用期已过的话，无法进入软件的 help 页签，这里提供一下几种临时激活的方法。请使用临时激活后，然后用插件激活，即可永久使用。</p><p><strong>温馨提示：激活码激活过程中，请断开网络连接！</strong></p><p><strong>激活码</strong></p><p>2022激活jetBrains通用版：<a href="https://mano100.cn/thread-1942-1-1.html">https://mano100.cn/thread-1942-1-1.html</a></p><blockquote><p><strong>网盘激活码(已废弃)</strong></p><p><a href="https://pan.baidu.com/s/1LBh66mqG19DkAlU9gP1QCg?pwd=o9id">https://pan.baidu.com/s/1LBh66mqG19DkAlU9gP1QCg?pwd=o9id</a></p><p><strong>账号激活(已废弃)</strong></p><p><a href="https://pan.baidu.com/s/1Gw_kYzP6HPeJBiBVCBZEnw?pwd=2233">https://pan.baidu.com/s/1Gw_kYzP6HPeJBiBVCBZEnw?pwd=2233</a></p><p><a href="https://pan.baidu.com/s/1wUdDvnQ9tthvXMUAB_k7WQ?pwd=Hh23">https://pan.baidu.com/s/1wUdDvnQ9tthvXMUAB_k7WQ?pwd=Hh23</a></p></blockquote><h3 id="建议">建议</h3><p><strong>可以在你的host文件里加上以下内容</strong></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><span class="line">127.0.0.1 account.jetbrains.com</span><br><span class="line">127.0.0.1 www.jetbrains.com</span><br></pre></td></tr></table></figure><h2 id="参考资料">参考资料</h2><ul><li>ja-netfilter插件下载：<a href="https://gitee.com/code_soft/ja-netfilter">https://gitee.com/code_soft/ja-netfilter</a></li><li>爱激活：<a href="https://www.ajihuo.com/">https://www.ajihuo.com/</a></li><li>视频演示激活jetBrains方法：<a href="https://www.ixigua.com/7054789540506141221?wid_try=1">https://www.ixigua.com/7054789540506141221?wid_try=1</a></li></ul><h2 id="常见问题">常见问题</h2><p>如果这个方法激活不了，你也可以试试这个链接 <a href="https://33tool.com/idea">https://33tool.com/idea</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在 &lt;a href=&quot;https://blog.mailjob.net/posts/2414163873.html&quot;&gt;Jetbrains 产品无限重置方法&lt;/a&gt; 我们讲了使用 &lt;code&gt;IDE Eval Reset&lt;/code&gt; 插件激活 Jetbrains 产品的方法</summary>
      
    
    
    
    <category term="blog" scheme="https://blog.mailjob.net/categories/blog/"/>
    
    
    <category term="blog" scheme="https://blog.mailjob.net/tags/blog/"/>
    
  </entry>
  
  <entry>
    <title>Go实现gRPC</title>
    <link href="https://blog.mailjob.net/posts/1433951833.html"/>
    <id>https://blog.mailjob.net/posts/1433951833.html</id>
    <published>2022-01-16T12:59:28.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言">引言</h2><p>在 <a href="https://blog.mailjob.net/posts/3190342588.html">Go原生方法实现RPC</a> 文章中，我们通过原生的方法实现了 RPC 调用。但是大多是基于 protobuf 进行 RPC 的实现。</p><p>gRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。gRPC基于HTTP/2协议设计，可以基于一个HTTP/2链接提供多个服务，对于移动设备更加友好。本节将讲述gRPC的简单用法。</p><p>gRPC技术栈：</p><p><img src="http://img.github.mailjob.net/20220116210428.png" alt="grpc技术栈"></p><p>最底层为TCP或Unix Socket协议，在此之上是HTTP/2协议的实现，然后在HTTP/2协议之上又构建了针对Go语言的gRPC核心库。应用程序通过gRPC插件生产的Stub代码和gRPC核心库通信，也可以直接和gRPC核心库通信。</p><h2 id="参考文献">参考文献</h2><blockquote><p>protobuf入门学习：<a href="https://github.com/mailjobblog/dev_go/tree/master/220115_protobuf">https://github.com/mailjobblog/dev_go/tree/master/220115_protobuf</a><br>Go原生RPC+protobuf代码下载：<a href="https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/3.rpc_protobuf">https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/3.rpc_protobuf</a><br>gRPC代码下载：<a href="https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/4.grpc">https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/4.grpc</a></p></blockquote><h2 id="RPC实现">RPC实现</h2><h3 id="Go原生rpc-proto实现">Go原生rpc+proto实现</h3><h4 id="代码实现">代码实现</h4><p>hello.proto</p><figure class="highlight protobuf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">syntax = <span class="string">&quot;proto3&quot;</span>;</span><br><span class="line"><span class="keyword">package</span> pb;</span><br><span class="line"><span class="keyword">option</span> go_package=<span class="string">&quot;./pb;pb&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 请求结构体</span></span><br><span class="line"><span class="keyword">message </span><span class="title class_">HelloRequest</span> &#123;</span><br><span class="line">  <span class="type">string</span> res = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回结构体</span></span><br><span class="line"><span class="keyword">message </span><span class="title class_">HelloResponse</span> &#123;</span><br><span class="line">  <span class="type">int64</span> reply = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>server.go</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">rpc.Register(<span class="built_in">new</span>(HelloService))</span><br><span class="line">listener, err := net.Listen(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;ListenTCP error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">conn, err := listener.Accept()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;Accept error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">go</span> rpc.ServeConn(conn)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> HelloService <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Length 和原生相比，这里的接收参数和返回参数都用的是proto生成的代码</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *HelloService)</span></span> Length(res pb.HelloRequest, reply *pb.HelloResponse) <span class="type">error</span> &#123;</span><br><span class="line">reply.Reply = <span class="type">int64</span>(<span class="built_in">len</span>(res.Res))</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>client_test.go</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestClient</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">client, err := rpc.Dial(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;127.0.0.1:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;dialing:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义请求和接受参数</span></span><br><span class="line"><span class="comment">// 接收参数和返回参数都用的是proto生成的代码</span></span><br><span class="line">res := pb.HelloRequest&#123;Res: <span class="string">&quot;test666&quot;</span>&#125;</span><br><span class="line"><span class="keyword">var</span> reply pb.HelloResponse</span><br><span class="line"></span><br><span class="line">err = client.Call(<span class="string">&quot;HelloService.Length&quot;</span>, res, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(reply)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="gRPC实现">gRPC实现</h3><h4 id="代码实现-2">代码实现</h4><p>hello.proto</p><figure class="highlight protobuf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">syntax = <span class="string">&quot;proto3&quot;</span>;</span><br><span class="line"><span class="keyword">package</span> pb;</span><br><span class="line"><span class="keyword">option</span> go_package=<span class="string">&quot;./pb;pb&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 请求结构体</span></span><br><span class="line"><span class="keyword">message </span><span class="title class_">HelloRequest</span> &#123;</span><br><span class="line">  <span class="type">string</span> res = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 返回结构体</span></span><br><span class="line"><span class="keyword">message </span><span class="title class_">HelloResponse</span> &#123;</span><br><span class="line">  <span class="type">int64</span> reply = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// GRPC服务</span></span><br><span class="line"><span class="keyword">service </span><span class="title class_">HelloService</span> &#123;</span><br><span class="line">  <span class="comment">// 计算字符串长度</span></span><br><span class="line">  <span class="function"><span class="keyword">rpc</span> Length(HelloRequest) <span class="keyword">returns</span> (HelloResponse)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>server.go</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="comment">// 创建一个 grpc server</span></span><br><span class="line">grpcServer := grpc.NewServer()</span><br><span class="line"><span class="comment">// 注册 grpc</span></span><br><span class="line">pb.RegisterHelloServiceServer(grpcServer, <span class="built_in">new</span>(HelloService))</span><br><span class="line"></span><br><span class="line">lis, err := net.Listen(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 监听端口上提供gRPC服务</span></span><br><span class="line">grpcServer.Serve(lis)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> HelloService <span class="keyword">struct</span> &#123;</span><br><span class="line">pb.UnimplementedHelloServiceServer</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *HelloService)</span></span> Length(ctx context.Context, res *pb.HelloRequest) (*pb.HelloResponse, <span class="type">error</span>) &#123;</span><br><span class="line">reply := &amp;pb.HelloResponse&#123;Reply: <span class="type">int64</span>(<span class="built_in">len</span>(res.Res))&#125;</span><br><span class="line"><span class="keyword">return</span> reply, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>client_test.go</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestClient</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">conn, err := grpc.Dial(<span class="string">&quot;127.0.0.1:8888&quot;</span>, grpc.WithTransportCredentials(insecure.NewCredentials()))</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">defer</span> conn.Close()</span><br><span class="line">client := pb.NewHelloServiceClient(conn)</span><br><span class="line"></span><br><span class="line">reply, err := client.Length(context.Background(), &amp;pb.HelloRequest&#123;Res: <span class="string">&quot;test123456&quot;</span>&#125;)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(reply.Reply)</span><br><span class="line">fmt.Println(reply.GetReply())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;https://blog.mailjob.net/posts/3190342588.html&quot;&gt;Go原生方法实现RPC&lt;/a&gt; 文章中，我们通过原生的方法实现了 RPC 调用。但是大多是基于 protobuf 进</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>Go原生方法实现RPC</title>
    <link href="https://blog.mailjob.net/posts/3190342588.html"/>
    <id>https://blog.mailjob.net/posts/3190342588.html</id>
    <published>2022-01-13T12:54:19.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言">引言</h2><p>RPC是远程过程调用的缩写（Remote Procedure Call），通俗地说就是调用远处的一个函数。使得应用程序之间可以进行通信，而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。<br><img src="http://img.github.mailjob.net/20220113211735.png" alt="RPC原理图"></p><h2 id="参考文献">参考文献</h2><blockquote><p>net/rpc实现rpc代码：<a href="https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/1.rpc">https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/1.rpc</a><br>jsonrpc实现rpc代码：<a href="https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/2.rpcjson">https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/2.rpcjson</a></p></blockquote><h2 id="RPC实现">RPC实现</h2><h3 id="Go原生net-rpc实现">Go原生net/rpc实现</h3><p>Go语言的RPC包的路径为<code>net/rpc</code>，也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。</p><h4 id="代码实现">代码实现</h4><p><strong>server.go</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">_ = rpc.Register(<span class="built_in">new</span>(HelloService))</span><br><span class="line"><span class="comment">// _ = rpc.RegisterName(&quot;HelloService&quot;, new(HelloService)) // 自定义名称注册</span></span><br><span class="line"></span><br><span class="line">listener, err := net.Listen(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;ListenTCP error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">conn, err := listener.Accept()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;Accept error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line">rpc.ServeConn(conn)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> HelloService <span class="keyword">struct</span> &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *HelloService)</span></span> Length(res <span class="type">string</span>, reply *<span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">*reply = <span class="built_in">len</span>(res)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数，所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接，并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。</p><p><strong>其中Length方法必须满足Go语言的RPC规则</strong>：</p><ul><li>方法只能有两个可序列化的参数</li><li>其中第二个参数是指针类型，</li><li>并且返回一个error类型</li><li>必须是公开的方法。</li></ul><p><strong>client.go</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">client, err := rpc.Dial(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;127.0.0.1:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;dialing:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">res := <span class="string">&quot;test HelloService&quot;</span></span><br><span class="line"><span class="keyword">var</span> reply <span class="type">int</span></span><br><span class="line">err = client.Call(<span class="string">&quot;HelloService.Length&quot;</span>, res, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fmt.Println(reply)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Go使用jsonrpc实现">Go使用jsonrpc实现</h3><p><strong>标准库的RPC默认采用Go语言特有的 <a href="https://blog.csdn.net/libinemail/article/details/122503469">gob</a> 编码，因此从其它语言调用Go语言实现的RPC服务将比较困难。</strong></p><p>Go语言的RPC框架有两个比较有特色的设计：一个是RPC数据打包时可以通过插件实现自定义的编码和解码；另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的，我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。</p><h4 id="代码实现-2">代码实现</h4><p><strong>server.go</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">_ = rpc.Register(<span class="built_in">new</span>(HelloService))</span><br><span class="line"></span><br><span class="line">listener, err := net.Listen(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;ListenTCP error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">conn, err := listener.Accept()</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;Accept error:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 用rpc.ServeCodec函数替代了rpc.ServeConn函数</span></span><br><span class="line"><span class="comment">// 传入的参数是针对服务端的json编解码器</span></span><br><span class="line"><span class="keyword">go</span> rpc.ServeCodec(jsonrpc.NewServerCodec(conn))</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> HelloService <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *HelloService)</span></span> Length(res <span class="type">string</span>, reply *<span class="type">int</span>) <span class="type">error</span> &#123;</span><br><span class="line">*reply = <span class="built_in">len</span>(res)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>client.go</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"><span class="comment">// 手工调用net.Dial函数建立TCP链接</span></span><br><span class="line">conn, err := net.Dial(<span class="string">&quot;tcp&quot;</span>, <span class="string">&quot;127.0.0.1:8888&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(<span class="string">&quot;net.Dial:&quot;</span>, err)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 针对客户端的json编解码器</span></span><br><span class="line">client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))</span><br><span class="line"></span><br><span class="line">res := <span class="string">&quot;test HelloService&quot;</span></span><br><span class="line"><span class="keyword">var</span> reply <span class="type">int</span></span><br><span class="line">err = client.Call(<span class="string">&quot;HelloService.Length&quot;</span>, res, &amp;reply)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">log.Fatal(err)</span><br><span class="line">&#125;</span><br><span class="line">fmt.Println(reply)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="知识拓展">知识拓展</h4><h5 id="查看服务器接收的客户端数据">查看服务器接收的客户端数据</h5><p>为了查看到客户端调用时发送的数据格式。我们可以用 <a href="https://www.runoob.com/linux/linux-comm-nc.html">nc</a> 命令 <code>nc -l 8888</code> 在同样的端口启动一个TCP服务。</p><p>然后再次执行一次RPC调用（client.go）将会发现nc输出了以下的信息：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;method&quot;</span><span class="punctuation">:</span><span class="string">&quot;HelloService.Length&quot;</span><span class="punctuation">,</span><span class="attr">&quot;params&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;test HelloService&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这是一个json编码的数据，其中<code>method</code>部分对应要调用的rpc服务和方法组合成的名字，<code>params</code>部分的第一个元素为参数，<code>id</code>是由调用端维护的一个唯一的调用编号。</p><p>请求的json数据对象在内部对应两个结构体：客户端是clientRequest，服务端是serverRequest。</p><p>clientRequest 和 serverRequest结构体的内容基本是一致的：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> clientRequest <span class="keyword">struct</span> &#123;</span><br><span class="line">    Method <span class="type">string</span>         <span class="string">`json:&quot;method&quot;`</span></span><br><span class="line">    Params [<span class="number">1</span>]<span class="keyword">interface</span>&#123;&#125; <span class="string">`json:&quot;params&quot;`</span></span><br><span class="line">    Id     <span class="type">uint64</span>         <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> serverRequest <span class="keyword">struct</span> &#123;</span><br><span class="line">    Method <span class="type">string</span>           <span class="string">`json:&quot;method&quot;`</span></span><br><span class="line">    Params *json.RawMessage <span class="string">`json:&quot;params&quot;`</span></span><br><span class="line">    Id     *json.RawMessage <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="用linux命令模拟客户端请求服务端">用linux命令模拟客户端请求服务端</h5><p>在获取到RPC调用对应的json数据后，我们可以通过直接向架设了RPC服务（server.go）的TCP服务器发送json数据模拟RPC方法调用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">echo</span> -e <span class="string">&#x27;&#123;&quot;method&quot;:&quot;HelloService.Length&quot;,&quot;params&quot;:[&quot;test HelloService&quot;],&quot;id&quot;:0&#125;&#x27;</span> | nc 127.0.0.1 8888</span><br></pre></td></tr></table></figure><p>返回的结果也是一个json格式的数据：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;result&quot;</span><span class="punctuation">:</span><span class="number">17</span><span class="punctuation">,</span><span class="attr">&quot;error&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>返回的json数据也是对应内部的两个结构体：客户端是clientResponse，服务端是serverResponse。两个结构体的内容同样也是类似的：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> clientResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">    Id     <span class="type">uint64</span>           <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Result *json.RawMessage <span class="string">`json:&quot;result&quot;`</span></span><br><span class="line">    Error  <span class="keyword">interface</span>&#123;&#125;      <span class="string">`json:&quot;error&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">type</span> serverResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">    Id     *json.RawMessage <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">    Result <span class="keyword">interface</span>&#123;&#125;      <span class="string">`json:&quot;result&quot;`</span></span><br><span class="line">    Error  <span class="keyword">interface</span>&#123;&#125;      <span class="string">`json:&quot;error&quot;`</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此无论采用何种语言，只要遵循同样的json结构，以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;
&lt;p&gt;RPC是远程过程调用的缩写（Remote Procedure Call），通俗地说就是调用远处的一个函数。使得应用程序之间可以进行通信，而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
  <entry>
    <title>etcd介绍与安装</title>
    <link href="https://blog.mailjob.net/posts/1765858488.html"/>
    <id>https://blog.mailjob.net/posts/1765858488.html</id>
    <published>2022-01-06T13:44:51.000Z</published>
    <updated>2026-07-25T09:01:29.899Z</updated>
    
    <content type="html"><![CDATA[<h2 id="介绍">介绍</h2><p>etcd是一个高可用的 Key/Value 存储系统，主要用于分享配置和服务发现。它是一款云原生时代的首选元数据存储产品，已经成为云原生和分布式系统的存储基石。类似项目有zookeeper和consul。</p><p><strong>etcd具有以下特点</strong>：</p><ul><li>完全复制：集群中的每个节点都可以使用完整的存档</li><li>高可用性：Etcd可用于避免硬件的单点故障或网络问题</li><li>一致性：每次读取都会返回跨多主机的最新写入</li><li>简单：包括一个定义良好、面向用户的API（gRPC）</li><li>安全：实现了带有可选的客户端证书身份验证的自动化TLS</li><li>快速：每秒10000次写入的基准速度</li><li>可靠：使用Raft算法实现了强一致、高可用的服务存储目录</li></ul><h3 id="etcd架构介绍">etcd架构介绍</h3><p><img src="http://img.github.mailjob.net/20220106223904.png" alt=""></p><h4 id="Client层">Client层</h4><ul><li>组成<ul><li>client v2 API 客户端库</li><li>client v3 API 客户端库</li></ul></li><li>作用<ul><li>提供了简洁易用的 API</li><li>支持负载均衡、节点间故障自动转移</li><li>极大降低业务使用 etcd 复杂度，提升开发效率、服务可用性</li></ul></li></ul><h4 id="API-网络层">API 网络层</h4><ul><li>client 访问 server的通信协议<ul><li>v2API<ul><li>HTTP/1.x 协议</li></ul></li><li>v3API<ul><li>gRPC 协议</li><li>HTTP/1.x 协议（通过 etcd grpc-gateway 组件支持）</li></ul></li></ul></li><li>server 节点之间的通信协议<ul><li>节点间通过 Raft 算法实现数据复制和 Leader 选举等功能时使用的 HTTP 协议</li></ul></li></ul><h4 id="Raft-算法层（基石和亮点）">Raft 算法层（基石和亮点）</h4><ul><li>核心算法组成<ul><li>Leader 选举</li><li>日志复制</li><li>ReadIndex</li></ul></li><li>作用<ul><li>用于保障 etcd 多个节点间的数据一致性、提升服务可用性等</li></ul></li></ul><h4 id="功能逻辑层（核心特性实现层）">功能逻辑层（核心特性实现层）</h4><ul><li><p>KVServer 模块</p><ul><li>限速判断（保证集群稳定性，避免雪崩）</li></ul><p><img src="http://img.github.mailjob.net/20220106223946.png" alt=""></p><ul><li>生成一个唯一的 ID，将此请求关联到一个对应的消息通知 channel，然后向 Raft 模块发起（Propose）一个提案（Proposal）</li><li>等待此 put 请求，等待写入结果通过消息通知 channel 返回或者超时。etcd 默认超时时间是 7 秒（5 秒磁盘 IO 延时 +2*1 秒竞选超时时间），如果一个请求超时未返回结果，则可能会出现 etcdserver: request timed out 错误</li></ul></li><li><p>MVCC 模块</p><ul><li><p>读场景</p><p><img src="http://img.github.mailjob.net/20220106224032.jpeg" alt="img"></p></li><li><p>写场景</p><p><img src="http://img.github.mailjob.net/20220106224040.png" alt=""></p></li><li><p>treeIndex 模块（内存树形索引模块）</p><ul><li>保存用户 key 和版本号的映射关系</li></ul></li><li><p>boltdb 模块</p><ul><li>基于 B+ tree 实现的 key-value 键值库，支持事务，提供 Get/Put 等简易 API 给 etcd 操作</li><li>每次修改操作，生成一个新的版本号 (revision)，以版本号为 key， value 为用户 key-value 等信息组成的结构体</li><li>数据隔离<ul><li>boltdb 里每个 bucket 类似对应 MySQL 一个表<ul><li>用户的 key 数据存放的 bucket 名字的是 key</li><li>etcd MVCC 元数据存放的 bucket 是 meta</li></ul></li></ul></li></ul></li></ul></li><li><p>Auth 鉴权模块</p></li><li><p>Lease 租约模块</p></li><li><p>Compactor 压缩模块</p></li><li><p>Quota 模块（配额）</p></li><li><p>Apply模块</p><p><img src="http://img.github.mailjob.net/20220106224130.png" alt=""></p></li></ul><h4 id="存储层">存储层</h4><ul><li>预写日志 (WAL) 模块<ul><li>可保障 etcd crash 后数据不丢失</li></ul></li><li>快照 (Snapshot) 模块</li><li>boltdb 模块<ul><li>保存了集群元数据和用户写入的数据</li></ul></li></ul><h2 id="参考文献">参考文献</h2><ul><li>etcd官方下载：<a href="https://github.com/etcd-io/etcd/releases">https://github.com/etcd-io/etcd/releases</a></li><li>etcd集群搭建：<a href="https://www.wenjiangs.com/doc/h30rpjp5r">https://www.wenjiangs.com/doc/h30rpjp5r</a></li><li>etcd集群配置文件：<a href="https://www.cnblogs.com/skymyyang/p/9067280.html">https://www.cnblogs.com/skymyyang/p/9067280.html</a></li></ul><h2 id="安装与配置">安装与配置</h2><p>本文基于 <code>centos7</code> 和  <code>etcd v3.5.1</code> 进行讲解，请注意版本软件区分。</p><h3 id="安装">安装</h3><p><strong>下载etcd</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -O  https://github.com/etcd-io/etcd/releases/download/v3.5.1/etcd-v3.5.1-linux-amd64.tar.gz</span><br></pre></td></tr></table></figure><p>PS：curl默认不支持Https，命令<code>curl -V</code>（V大写）查看Protocols项有没有https ，如果没有就要用命令：<code>yum install openssl-devel</code> 装SSL</p><p><strong>解压文件</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tar -zxvf etcd-v3.5.1-linux-amd64.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> etcd-v3.5.1-linux-amd6</span><br></pre></td></tr></table></figure><p><strong>添加环境变量</strong></p><p>etcd是服务端，etcdctl是运维人员操作的控制端，一般只需要装etcd，现在是学习就都装在同一台机器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> etcd /usr/local/bin</span><br><span class="line"><span class="built_in">cp</span> etcdctl /usr/local/bin</span><br></pre></td></tr></table></figure><p>PS：用echo $PATH查看自己的环境变量路径</p><p><strong>修改环境变量PATH</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/profile</span><br></pre></td></tr></table></figure><p>在文件最后加入变量，因为<code>etcd</code>默认使用<code>V2</code>版本，我们需要<code>V3</code>版本的<code>API</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> ETCDCTL_API=3</span><br></pre></td></tr></table></figure><p>刷新环境变量</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">source</span> /etc/profile</span><br></pre></td></tr></table></figure><p><strong>查看版本信息</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">etcd -version</span><br><span class="line">或</span><br><span class="line">etcdctl version</span><br></pre></td></tr></table></figure><h3 id="启动etcd">启动etcd</h3><h4 id="单机部署">单机部署</h4><p><strong>启动</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">etcd</span><br></pre></td></tr></table></figure><p>直接运行命令 <code>./etcd</code> 就可以启动了，非常简单。默认使用2379端口为客户端提供通讯， 并使用端口2380来进行服务器间通讯。</p><p><strong>测试</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">[root@VM-0-15-centos ~]# etcdctl put foo bar</span><br><span class="line">OK</span><br><span class="line">[root@VM-0-15-centos ~]# etcdctl get foo</span><br><span class="line">foo</span><br><span class="line">bar</span><br></pre></td></tr></table></figure><h4 id="集群部署">集群部署</h4><p>内容略去。。。。。。</p><h3 id="其他配置">其他配置</h3><p><strong>创建etcd配置文件</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[root@VM-0-15-centos ~]# <span class="built_in">mkdir</span> -p /var/lib/etcd/</span><br><span class="line">[root@VM-0-15-centos ~]# <span class="built_in">cat</span> &lt;&lt;<span class="string">EOF | sudo tee /etc/etcd.conf</span></span><br><span class="line"><span class="string">&gt; # 节点名称</span></span><br><span class="line"><span class="string">&gt; ETCD_NAME=etcd1</span></span><br><span class="line"><span class="string">&gt; # 数据存放位置</span></span><br><span class="line"><span class="string">&gt; ETCD_DATA_DIR=/var/lib/etcd/</span></span><br><span class="line"><span class="string">&gt; EOF</span></span><br></pre></td></tr></table></figure><p><strong>创建开机启动文件</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim  /etc/systemd/system/etcd.service</span><br></pre></td></tr></table></figure><p>写入以下配置信息</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=Etcd Server</span><br><span class="line">Documentation=https://github.com/coreos/etcd</span><br><span class="line">After=network.target</span><br><span class="line"> </span><br><span class="line">[Service]</span><br><span class="line">User=root</span><br><span class="line">Type=notify</span><br><span class="line">#这个文件特别关键，etcd使用的环境变量都需要通过环境变量文件读取</span><br><span class="line">EnvironmentFile=-/etc/etcd.conf</span><br><span class="line">ExecStart=/usr/local/bin/etcd</span><br><span class="line">Restart=on-failure</span><br><span class="line">RestartSec=10s</span><br><span class="line">LimitNOFILE=40000</span><br><span class="line"> </span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><p>PS：<code>EnvironmentFile=-/etc/etcd.conf </code>这个配置项里<code>/etc</code>前的<code>-</code>是<code>K8S</code>生成配置文件时就有的，不是我误写的。</p><p><strong>重新加载配置 &amp;&amp; 开机启动 &amp;&amp; 启动etcd</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl daemon-reload &amp;&amp; systemctl <span class="built_in">enable</span> etcd &amp;&amp; systemctl start etcd</span><br></pre></td></tr></table></figure><p>开机启动设置状态，状态<code>enabled</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><span class="line">[root@VM-0-15-centos ~]# systemctl list-unit-files etcd.service</span><br><span class="line">UNIT FILE    STATE  </span><br><span class="line">etcd.service enabled</span><br><span class="line"></span><br><span class="line">1 unit files listed.</span><br></pre></td></tr></table></figure><p>查看etcd状态</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl show etcd.service</span><br></pre></td></tr></table></figure><p><strong>查看2379端口是否启动成功</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[root@VM-0-15-centos ~]# netstat -an |grep 2379</span><br><span class="line">tcp        0      0 127.0.0.1:2379          0.0.0.0:*               LISTEN     </span><br><span class="line">tcp        0      0 127.0.0.1:50822         127.0.0.1:2379          ESTABLISHED</span><br><span class="line">tcp        0      0 127.0.0.1:2379          127.0.0.1:50822         ESTABLISHED</span><br></pre></td></tr></table></figure><p>PS： CentOS默认没有装<code>netstat</code>，需要 <code>yum install -y net-tools</code> 自己装</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;介绍&quot;&gt;介绍&lt;/h2&gt;
&lt;p&gt;etcd是一个高可用的 Key/Value 存储系统，主要用于分享配置和服务发现。它是一款云原生时代的首选元数据存储产品，已经成为云原生和分布式系统的存储基石。类似项目有zookeeper和consul。&lt;/p&gt;
&lt;p&gt;&lt;strong</summary>
      
    
    
    
    <category term="linux" scheme="https://blog.mailjob.net/categories/linux/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
    <category term="linux" scheme="https://blog.mailjob.net/tags/linux/"/>
    
    <category term="etcd" scheme="https://blog.mailjob.net/tags/etcd/"/>
    
  </entry>
  
  <entry>
    <title>Go工程化:函数式选项模式</title>
    <link href="https://blog.mailjob.net/posts/485460363.html"/>
    <id>https://blog.mailjob.net/posts/485460363.html</id>
    <published>2021-12-23T05:06:00.000Z</published>
    <updated>2026-07-25T09:01:29.897Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言">引言</h2><p>Go 不是完全面向对象语言，有一些面向对象模式不太适合它。但经过这些年的发展，Go 有自己的一些模式。今天介绍一个常见的模式：函数式选项模式（Functional Options Pattern）。</p><h2 id="参考文献">参考文献</h2><p>引用官方博文：<a href="https://golang.cafe/blog/golang-functional-options-pattern.html">https://golang.cafe/blog/golang-functional-options-pattern.html</a><br>本文代码下载：<a href="https://github.com/mailjobblog/dev_go/tree/master/211223-FunOptionPattern">https://github.com/mailjobblog/dev_go/tree/master/211223-FunOptionPattern</a></p><h2 id="函数式选项模式介绍">函数式选项模式介绍</h2><p>Go 语言没有构造函数，一般通过定义 New 函数来充当构造函数。然而，如果结构有较多字段，要初始化这些字段，有很多种方式，但有一种方式认为是最好的，这就是函数式选项模式（Functional Options Pattern）。</p><p>函数式选项模式是一种在 Go 中构造结构体的模式，它通过设计一组非常有表现力和灵活的 API 来帮助配置和初始化结构体。</p><p>在 Uber 的 Go 语言规范中提到了该模式：</p><blockquote><p>Functional options 是一种模式，在该模式中，你可以声明一个不透明的 <code>Option</code> 类型，该类型在某些内部结构中记录信息。你接受这些可变数量的选项，并根据内部结构上的选项记录的完整信息进行操作。</p><p>将此模式用于构造函数和其他公共 API 中的可选参数，你预计这些参数需要扩展，尤其是在这些函数上已经有三个或更多参数的情况下。</p></blockquote><h2 id="代码演示">代码演示</h2><p>为了更好的理解该模式，我们通过一个例子来讲解。定义一个 Server 结构体：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Server <span class="keyword">struct</span>&#123;</span><br><span class="line">  host <span class="type">string</span></span><br><span class="line">  port <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(host <span class="type">string</span>, port <span class="type">int</span>)</span></span> *Server &#123;</span><br><span class="line">  <span class="keyword">return</span> &amp;Server&#123;host, port&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span></span> Start() <span class="type">error</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如何使用呢？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">&quot;log&quot;</span></span><br><span class="line">  <span class="string">&quot;server&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  svr := New(<span class="string">&quot;localhost&quot;</span>, <span class="number">1234</span>)</span><br><span class="line">  <span class="keyword">if</span> err := svr.Start(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    log.Fatal(err)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但如果要扩展 Server 的配置选项，如何做？通常有三种做法：</p><ul><li>为每个不同的配置选项声明一个新的构造函数</li><li>定义一个新的 Config 结构体来保存配置信息</li><li>使用 Functional Option Pattern</li></ul><p>下面，我们将对于这三种模式，进行详细的讲解和演示：</p><h3 id="方法一">方法一</h3><p>这种做法是在构造函数上，定义配置参数。有几个配置参数，则在 New 上就配置几个参数。用这种方式，当配置少的时候没有问题。但是，如果配置项比较多的时候将会是灾难。</p><p>这种方式的应用。比如 net 包中的 Dial 和 DialTimeout：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Dial</span><span class="params">(network, address <span class="type">string</span>)</span></span> (Conn, <span class="type">error</span>)</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">DialTimeout</span><span class="params">(network, address <span class="type">string</span>, timeout time.Duration)</span></span> (Conn, <span class="type">error</span>)</span><br></pre></td></tr></table></figure><p><strong>完整演示代码示例</strong>：</p><p>Pkg.server</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Server <span class="keyword">struct</span> &#123;</span><br><span class="line">host <span class="type">string</span></span><br><span class="line">port <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(host <span class="type">string</span>, port <span class="type">int</span>)</span></span> *Server &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;Server&#123;host, port&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TestFunc 测试包内函数调用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span></span> TestFunc() (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;Run Success, host:%s, port:%d \n&quot;</span>, s.host, s.port), <span class="literal">nil</span> <span class="comment">// test fun return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>main</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := server.New(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">1234</span>)</span><br><span class="line">test, err := s.TestFunc()</span><br><span class="line">fmt.Println(test, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方法二">方法二</h3><p>这种方式也是很常见的，特别是当配置选项很多时。通常可以创建一个 <code>Config</code> 结构体，其中包含 <code>Server</code> 的所有配置选项。这种做法，即使将来增加更多配置选项，也可以轻松的完成扩展，不会破坏 <code>Server</code> 的 API。</p><p>在使用时，需要先构造 <code>Config</code> 实例，对这个实例，又回到了前面 <code>Server </code>的问题上，因为增加或删除选项，需要对 <code>Config</code> 有较大的修改。如果将 <code>Config</code> 中的字段改为私有，可能需要定义 <code>Config</code> 的构造函数。。。</p><p><strong>完整演示代码示例</strong>：</p><p>Pkg.server</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Server <span class="keyword">struct</span> &#123;</span><br><span class="line">cfg Config</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Config <span class="keyword">struct</span> &#123;</span><br><span class="line">Host <span class="type">string</span></span><br><span class="line">Port <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(cfg Config)</span></span> *Server &#123;</span><br><span class="line"><span class="keyword">return</span> &amp;Server&#123;cfg&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TestFunc 测试包内函数调用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span></span> TestFunc() (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;Run Success, host:%s, port:%d \n&quot;</span>, s.cfg.Host, s.cfg.Port), <span class="literal">nil</span> <span class="comment">// test fun return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>main</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := server.New(server.Config&#123;</span><br><span class="line">Host: <span class="string">&quot;127.0.0.1&quot;</span>,</span><br><span class="line">Port: <span class="number">1234</span>,</span><br><span class="line">&#125;)</span><br><span class="line">test, err := s.TestFunc()</span><br><span class="line">fmt.Println(test, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方法三">方法三</h3><p>一个更好的解决方案是使用 <code>Functional Option Pattern</code>。在这个模式中，我们定义一个 <code>Option</code> 函数类型：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Option <span class="function"><span class="keyword">func</span><span class="params">(*Server)</span></span></span><br></pre></td></tr></table></figure><p><code>Option</code> 类型是一个函数类型，它接收一个参数：<code>*Server</code>。然后，<code>Server</code> 的构造函数接收一个 <code>Option</code> 类型的不定参数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(options ...Option)</span></span> *Server &#123;</span><br><span class="line">  svr := &amp;Server&#123;&#125;</span><br><span class="line">  <span class="keyword">for</span> _, f := <span class="keyword">range</span> options &#123;</span><br><span class="line">    f(svr)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> svr</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那选项如何起作用？需要定义一系列相关返回 Option 的函数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithHost</span><span class="params">(host <span class="type">string</span>)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">    s.host = host</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithPort</span><span class="params">(port <span class="type">int</span>)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">    s.port = port</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithTimeout</span><span class="params">(timeout time.Duration)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">    s.timeout = timeout</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithMaxConn</span><span class="params">(maxConn <span class="type">int</span>)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">    s.maxConn = maxConn</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将来增加选项，只需要增加对应的 WithXXX 函数即可。</p><p><strong>完整演示代码示例</strong>：</p><p>Pkg.server</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Server <span class="keyword">struct</span> &#123;</span><br><span class="line">host <span class="type">string</span></span><br><span class="line">port <span class="type">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Option <span class="function"><span class="keyword">func</span><span class="params">(*Server)</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">New</span><span class="params">(options ...Option)</span></span> *Server &#123;</span><br><span class="line">ser := &amp;Server&#123;&#125;</span><br><span class="line"><span class="keyword">for</span> _, f := <span class="keyword">range</span> options &#123;</span><br><span class="line">f(ser)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> ser</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithHost</span><span class="params">(host <span class="type">string</span>)</span></span> Option &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">s.host = host</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithPort</span><span class="params">(port <span class="type">int</span>)</span></span> Option &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(s *Server)</span></span> &#123;</span><br><span class="line">s.port = port</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TestFunc 测试包内函数调用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span></span> TestFunc() (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> fmt.Sprintf(<span class="string">&quot;Run Success, host:%s, port:%d \n&quot;</span>, s.host, s.port), <span class="literal">nil</span> <span class="comment">// test fun return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>main</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">s := server.New(</span><br><span class="line">server.WithHost(<span class="string">&quot;127.0.0.1&quot;</span>),</span><br><span class="line">server.WithPort(<span class="number">1234</span>),</span><br><span class="line">)</span><br><span class="line">test, err := s.TestFunc()</span><br><span class="line">fmt.Println(test, err)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="扩展阅读">扩展阅读</h2><p>这种模式，在第三方库中使用挺多，比如 <a href="https://github.com/gocolly/colly">github.com/gocolly/colly</a>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Collector &#123;</span><br><span class="line">  <span class="comment">// 省略...</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewCollector</span><span class="params">(options ...CollectorOption)</span></span> *Collector</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义了一系列 CollectorOpiton</span></span><br><span class="line"><span class="keyword">type</span> CollectorOption&#123;</span><br><span class="line">  <span class="comment">// 省略...</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">AllowURLRevisit</span><span class="params">()</span></span> CollectorOption</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">AllowedDomains</span><span class="params">(domains ...<span class="type">string</span>)</span></span> CollectorOption</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>不过 <code>Uber</code> 的 Go 语言编程规范中提到该模式时，建议定义一个 Option 接口，而不是 Option 函数类型。该 Option 接口有一个未导出的方法，然后通过一个未导出的 <code>options</code> 结构来记录各选项。</p><p>Uber 的这个例子能看懂吗？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> options <span class="keyword">struct</span> &#123;</span><br><span class="line">  cache  <span class="type">bool</span></span><br><span class="line">  logger *zap.Logger</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Option <span class="keyword">interface</span> &#123;</span><br><span class="line">  apply(*options)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> cacheOption <span class="type">bool</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c cacheOption)</span></span> apply(opts *options) &#123;</span><br><span class="line">  opts.cache = <span class="type">bool</span>(c)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithCache</span><span class="params">(c <span class="type">bool</span>)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> cacheOption(c)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> loggerOption <span class="keyword">struct</span> &#123;</span><br><span class="line">  Log *zap.Logger</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(l loggerOption)</span></span> apply(opts *options) &#123;</span><br><span class="line">  opts.logger = l.Log</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithLogger</span><span class="params">(log *zap.Logger)</span></span> Option &#123;</span><br><span class="line">  <span class="keyword">return</span> loggerOption&#123;Log: log&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Open creates a connection.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Open</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">  addr <span class="type">string</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">  opts ...Option,</span></span></span><br><span class="line"><span class="params"><span class="function">)</span></span> (*Connection, <span class="type">error</span>) &#123;</span><br><span class="line">  options := options&#123;</span><br><span class="line">    cache:  defaultCache,</span><br><span class="line">    logger: zap.NewNop(),</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> _, o := <span class="keyword">range</span> opts &#123;</span><br><span class="line">    o.apply(&amp;options)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;
&lt;p&gt;Go 不是完全面向对象语言，有一些面向对象模式不太适合它。但经过这些年的发展，Go 有自己的一些模式。今天介绍一个常见的模式：函数式选项模式（Functional Options Pattern）。&lt;/p&gt;
&lt;h2 id=&quot;参考文献&quot;</summary>
      
    
    
    
    <category term="go" scheme="https://blog.mailjob.net/categories/go/"/>
    
    
    <category term="go" scheme="https://blog.mailjob.net/tags/go/"/>
    
  </entry>
  
</feed>
