<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>打字员杨师傅</title>
    <link>https://yangshifu.xyz/</link>
    <description>Full Stack Bug Builder</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, 杨师傅</copyright>
    <lastBuildDate>Wed, 14 Oct 2026 11:11:08 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <atom:link href="https://yangshifu.xyz/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>[翻译]Pi Durable：持久化智能体运行框架</title>
      <link>https://yangshifu.xyz/posts/pi-durable/</link>
      <description>Pi Durable 中英对照：了解支持长期运行、崩溃恢复、并发对话、上下文压缩和多人协作的持久化智能体运行框架，以及扩展、任务和应用状态的设计。</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/AI/">AI</category>
      <category domain="https://yangshifu.xyz/tags/ai/">ai</category>
      <category domain="https://yangshifu.xyz/tags/pi/">pi</category>
      <category domain="https://yangshifu.xyz/tags/tech/">tech</category>
      <pubDate>Wed, 14 Oct 2026 11:11:08 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://earendil.com/posts/pi-durable/">Pi Durable</a></li><li>原作者：Earendil Engineering</li><li>原文发表于：2026-10-01</li><li>原文源码：<a target="_blank" rel="noopener" href="https://github.com/earendil-works/website/blob/main/posts/pi-durable.md">earendil-works&#x2F;website · posts&#x2F;pi-durable.md</a></li><li>原文许可：<a target="_blank" rel="noopener" href="https://github.com/earendil-works/website/blob/main/LICENSE">Apache-2.0</a>，许可来自原文网站源码仓库，全文附于文末。</li><li>翻译说明：本文添加中文译文及代码注释译文，原文站内链接改为绝对地址；终端演示保留原站入口及章节说明。代码逻辑和示例字符串保持原样，示例对应原文发布时的版本。</li></ul></blockquote><p>Today Earendil and the Pi community <a target="_blank" rel="noopener" href="https://earendil.com/posts/pi-1-0/">shipped Pi 1.0</a>. This reflects our belief that after countless hours of hardening, maintenance, and active development, Pi is now a solid foundation on which to build. Pi also continues to evolve. Together with Pi 1.0, we are shipping an experimental new package called Pi Durable. Pi Durable was built specifically for long-running, durable, and malleable agents that can run anywhere. We would like you to join in the fun and help us make it the best durable harness there is.</p><p>今天，Earendil 和 Pi 社区<a target="_blank" rel="noopener" href="https://earendil.com/posts/pi-1-0/">发布了 Pi 1.0</a>。这体现了我们的判断：经过无数小时的加固、维护和积极开发，Pi 已经成为一个坚实的构建基础。Pi 也仍在不断演进。在发布 Pi 1.0 的同时，我们还推出了一个名为 Pi Durable 的全新实验性软件包。Pi Durable 专为能够在任何地方运行、支持长期运行、具备持久性且易于改造的智能体而打造。我们希望你也加入进来，一起把它打造成最好的持久化智能体运行框架（harness）。</p><span id="more"></span><h2 id="why-pi-durable-为什么需要-pi-durable？"><a href="#why-pi-durable-为什么需要-pi-durable？" class="headerlink" title="Why Pi Durable? 为什么需要 Pi Durable？"></a>Why Pi Durable? 为什么需要 Pi Durable？</h2><p>Pi the coding agent is built to run on your (remote) machine, inside a terminal, driven by one person. If the process dies, you look at what happened and tell it to continue. That is what Pi 1.0 focuses on and excels at, and that is not changing.</p><p>Pi 编码智能体的设计目标是：在你的本地或远程机器上，在终端里运行，由一个人操控。如果进程终止了，你查看发生了什么，再让它继续。这正是 Pi 1.0 专注并擅长的事情，这一点不会改变。</p><p>At Earendil, we want to bring this technology to everyone, in whatever form fits their needs best. For that, we need a harness that runs anywhere, can be reached from different surfaces, supports infinitely long conversations, survives catastrophic internal and external failures, and lets multiple humans steer the same agents.</p><p>在 Earendil，我们希望把这项技术带给每个人，并以最符合各自需求的形式提供它。为此，我们需要一种运行框架：它能在任何地方运行，可以从不同的交互入口访问，支持无限长的对话，能够经受灾难性的内部和外部故障，还能让多人共同引导同一批智能体。</p><p>Pi Durable is that harness. It does not replace the Pi coding agent. It is a framework for building any agentic application, coding agents included. It shares not only code with the Pi coding agent, like pi-ai, but also its principles: minimalism and malleability.</p><p>Pi Durable 就是这样的运行框架。它不会取代 Pi 编码智能体，而是一个用于构建各种智能体应用的框架，其中也包括编码智能体。它与 Pi 编码智能体共享的不仅是 pi-ai 等代码，还有同样的理念：极简主义和可塑性。</p><p>It also lets us explore designs in this space without disrupting Pi the coding agent. Lessons we learn building agentic applications on Pi Durable will flow back into Pi the coding agent as they prove themselves valuable.</p><p>它还让我们能够探索这一领域的设计，而不干扰 Pi 编码智能体。当我们基于 Pi Durable 构建智能体应用时获得的经验被证明有价值，这些经验就会反馈到 Pi 编码智能体中。</p><h2 id="what-is-a-harness-什么是-harness？"><a href="#what-is-a-harness-什么是-harness？" class="headerlink" title="What is a harness? 什么是 harness？"></a>What is a harness? 什么是 harness？</h2><p>Everybody has their own definition of a harness. We <a target="_blank" rel="noopener" href="https://earendil.com/posts/what-is-a-harness/">wrote about this previously</a>, but let us reintroduce the concept of the harness for Pi Durable.</p><p>每个人对 harness 都有自己的定义。我们<a target="_blank" rel="noopener" href="https://earendil.com/posts/what-is-a-harness/">之前写过这个话题</a>，这里再结合 Pi Durable 重新介绍一下这个概念。</p><p>A harness is storage plus the machinery needed to run one or more conversations with large language models in parallel. It provides the tools those models call, and the execution environments the tools run in.</p><p>harness 是存储，以及并行运行一个或多个大语言模型对话所需的运行机制。它提供模型调用的工具，以及这些工具运行所在的执行环境。</p><p>A conversation is an interaction between you and an agent, recorded as a transcript. The agent is the large language model together with its settings, like the thinking level, and the tools it can call.</p><p>对话是你与智能体之间的交互，以对话记录（transcript）的形式保存。智能体则由大语言模型、思考级别等设置，以及它能调用的工具共同组成。</p><p>Tools do their work through an execution environment, which can be your laptop, a remote VM, or an in-memory sandbox. Which tools and which execution environment an agent gets is up to each conversation.</p><p>工具通过执行环境完成工作。这个环境可以是你的笔记本电脑、远程虚拟机，也可以是内存中的沙箱。智能体获得哪些工具、使用哪个执行环境，由每个对话各自决定。</p><p>Everything the harness runs, from calling the model to executing a tool, is a task.</p><p>运行框架执行的所有工作，从调用模型到执行工具，都是任务。</p><p>Like everything in Pi, Pi Durable is built so your agent can understand it. The entire source code, without tests, is about 15,000 lines, which comes out to about 150,000 tokens with GPT and about 250,000 with Claude. That’s the worst case. To build on Pi Durable, your agent rarely needs all of it; the storage backends alone are 3,000 lines it can usually skip.</p><p>与 Pi 的其他部分一样，Pi Durable 的设计也便于你的智能体理解。不含测试的全部源码约有 15,000 行，使用 GPT 分词约为 150,000 个 token，使用 Claude 分词约为 250,000 个 token。这是最坏的情况。基于 Pi Durable 构建应用时，你的智能体很少需要阅读全部源码；仅存储后端就有 3,000 行，通常可以跳过。</p><p>Now let us give you a little tour of Pi Durable, to illustrate what we built and why we built it.</p><p>接下来，让我们带你简单了解 Pi Durable，说明我们构建了什么，以及为什么这样构建。</p><h2 id="long-runs-anywhere-在任何地方长期运行"><a href="#long-runs-anywhere-在任何地方长期运行" class="headerlink" title="Long runs anywhere 在任何地方长期运行"></a>Long runs anywhere 在任何地方长期运行</h2><p>We want agents to run for a long time and to be able to run anywhere, where anywhere currently means anywhere there is a JavaScript runtime.</p><p>我们希望智能体能长时间运行，也能在任何地方运行。目前，所谓「任何地方」是指任何有 JavaScript 运行时的地方。</p><p>In Pi Durable, a harness opens over a storage backend. Pi Durable ships memory, SQLite, and JSONL storage, plus a conformance suite and benchmarks for your own backend. The SQLite and JSONL storage code uses no Node APIs, so with a small adapter it runs on Bun or inside a Cloudflare Durable Object. The storage interface is small and easy to implement on top of whatever you have, like a key-value store or Postgres. One process owns a storage at a time, and other clients attach to that process.</p><p>在 Pi Durable 中，运行框架基于一个存储后端打开。Pi Durable 自带内存、SQLite 和 JSONL 存储，还提供一致性测试套件和基准测试，供你验证自己的后端。SQLite 和 JSONL 存储代码没有使用任何 Node API，因此只需一个小型适配器，就能在 Bun 或 Cloudflare Durable Object 中运行。存储接口很小，很容易在你已有的系统上实现，例如键值存储或 Postgres。同一时刻只有一个进程拥有某个存储，其他客户端连接到这个进程。</p><p>On SQLite, the harness only keeps the working set in memory: the active transcripts, live tasks, and pending submissions. Everything else stays on disk until it is needed. Active transcripts are naturally bounded by the model’s context window, because compaction summarizes older messages before they overflow it. So even a conversation with tens of thousands of messages fits snugly into memory.</p><p>使用 SQLite 时，运行框架只把工作集保留在内存中：活跃的对话记录、正在运行的任务和待处理的提交。其他内容都留在磁盘上，直到需要时才读取。活跃对话记录的大小天然受模型上下文窗口约束，因为上下文压缩会在旧消息超出窗口之前对其进行摘要。因此，即使一个对话包含数万条消息，也能轻松容纳在内存中。</p><p>Tools that need files or a shell get them from an execution environment. Pi Durable ships a Node execution environment, which gives tools access to your local files. Like storage, the execution environment interface is small and easy to implement, so you can also expose remote execution environments to your tools. That allows the harness to run on one machine while its tools run on another. Your <code>env</code> function builds the environment for every tool call, from the conversation’s working directory, so each conversation can run in a different place.</p><p>需要文件或 shell 的工具从执行环境中获得这些能力。Pi Durable 自带 Node 执行环境，让工具可以访问本地文件。与存储接口一样，执行环境接口也很小且容易实现，因此你也可以向工具提供远程执行环境。这样，运行框架可以在一台机器上运行，而工具在另一台机器上运行。你的 <code>env</code> 函数会根据对话的工作目录，为每次工具调用构建环境，因此每个对话都可以在不同的地方运行。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; <span class="hljs-variable constant_">BACKGROUND_CONTEXT</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/chord/context&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; createModels &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-ai/models&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; openaiProvider &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-ai/providers/openai&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; createRegistry, <span class="hljs-title class_">Harness</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">NodeExecutionEnv</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable/env/node&quot;</span>;<br><span class="hljs-keyword">import</span> &#123;<br>    openNodeSqliteStorage,<br>&#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable/storage/sqlite/node&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">CodingTools</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable/tools&quot;</span>;<br><br><span class="hljs-keyword">const</span> context = <span class="hljs-variable constant_">BACKGROUND_CONTEXT</span>; <span class="hljs-comment">// every call takes a context for cancellation</span><br><span class="hljs-comment">// 每次调用都接收一个用于取消操作的上下文。</span><br><span class="hljs-keyword">const</span> models = <span class="hljs-title function_">createModels</span>();<br>models.<span class="hljs-title function_">setProvider</span>(<span class="hljs-title function_">openaiProvider</span>());<br><br><span class="hljs-keyword">const</span> registry = <span class="hljs-title function_">createRegistry</span>();<br>registry.<span class="hljs-title function_">install</span>(<span class="hljs-title class_">CodingTools</span>); <span class="hljs-comment">// read, write, edit, bash</span><br><span class="hljs-comment">// read、write、edit、bash 工具。</span><br><br><span class="hljs-keyword">const</span> <span class="hljs-title function_">env</span> = (<span class="hljs-params">&#123; cwd &#125;: &#123; cwd?: <span class="hljs-built_in">string</span> &#125;</span>) =&gt;<br>    <span class="hljs-keyword">new</span> <span class="hljs-title class_">NodeExecutionEnv</span>(&#123; <span class="hljs-attr">cwd</span>: cwd ?? process.<span class="hljs-title function_">cwd</span>() &#125;);<br><span class="hljs-keyword">const</span> harness = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Harness</span>.<span class="hljs-title function_">open</span>(<br>    <span class="hljs-keyword">await</span> <span class="hljs-title function_">openNodeSqliteStorage</span>(<span class="hljs-string">&quot;./agent.sqlite&quot;</span>),<br>    &#123; models, registry, env &#125;,<br>    context,<br>);<br><span class="hljs-comment">// The root conversation: created on first use, and the same one after every</span><br><span class="hljs-comment">// restart.</span><br><span class="hljs-comment">// 根对话：首次使用时创建，每次重启后仍是同一个对话。</span><br><span class="hljs-keyword">const</span> root = <span class="hljs-keyword">await</span> harness.<span class="hljs-title function_">root</span>(context, &#123;<br>    <span class="hljs-attr">agent</span>: &#123;<br>        <span class="hljs-attr">model</span>: &#123; <span class="hljs-attr">provider</span>: <span class="hljs-string">&quot;openai&quot;</span>, <span class="hljs-attr">modelId</span>: <span class="hljs-string">&quot;gpt-6.1-sol&quot;</span> &#125;,<br>        <span class="hljs-attr">cwd</span>: <span class="hljs-string">&quot;/work/repo&quot;</span>,<br>    &#125;,<br>&#125;);<br></code></pre></td></tr></table></figure><h2 id="survives-crashes-崩溃后仍能恢复"><a href="#survives-crashes-崩溃后仍能恢复" class="headerlink" title="Survives crashes 崩溃后仍能恢复"></a>Survives crashes 崩溃后仍能恢复</h2><p>We want an agent to survive its process dying, whether the laptop sleeps, the container is redeployed, or the machine runs out of memory, and to pick up where it left off.</p><p>我们希望智能体能在其进程终止后恢复运行，无论原因是笔记本电脑休眠、容器重新部署，还是机器内存耗尽，并且能够从中断的地方继续。</p><p>In Pi Durable, every step of a run is a task that stores a checkpoint before it moves on. If the process dies, a new process opens the same storage, finds the unfinished tasks, and continues each one from its last checkpoint. A model request that was cut off is sent again; the partial answer stays in the transcript, marked as aborted. A tool call that was cut off reruns if it is safe to; otherwise the model is told it was interrupted. Pi Durable has no built-in subagents, but they take a few lines of code to build, as the triage tool below shows. A subagent runs in a conversation of its own, so it continues from where it left off too, and a subagent tool that is safe to rerun finds its subagent again and waits for its answer. Queued messages are still queued. A <code>requestId</code> makes a submission exactly-once, so a client that retries after a crash gets the original submission back instead of asking twice.</p><p>在 Pi Durable 中，一次运行的每一步都是任务，在继续执行之前都会保存检查点。如果进程终止，新进程会打开同一个存储，找到未完成的任务，并让每个任务从最近的检查点继续。被中断的模型请求会重新发送；已经生成的部分回答保留在对话记录中，并标记为已中止。被中断的工具调用如果可以安全重跑，就会重新执行；否则，模型会被告知该调用已中断。Pi Durable 没有内置子智能体，但只需几行代码就能构建，下文的 triage 工具就展示了这一点。子智能体在自己的对话中运行，因此也能从中断的位置继续；如果一个子智能体工具可以安全重跑，它会重新找到自己的子智能体，等待它的回答。已排队的消息仍然留在队列中。<code>requestId</code> 确保一次提交只发生一次（exactly-once），因此客户端在崩溃后重试时会得到原来的提交，而不会重复提问。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">const</span> job = &#123;<br>    <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>,<br>    <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;Fix the flaky login test&quot;</span>,<br>    <span class="hljs-attr">requestId</span>: <span class="hljs-string">&quot;job-42&quot;</span>,<br>&#125; <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>;<br><span class="hljs-keyword">await</span> root.<span class="hljs-title function_">submit</span>(job, context);<br><span class="hljs-comment">// The process dies here, in the middle of a tool call.</span><br><span class="hljs-comment">// 进程在这里终止，此时正在执行一个工具调用。</span><br><br><span class="hljs-comment">// A new process opens the same storage.</span><br><span class="hljs-comment">// 新进程打开同一个存储。</span><br><span class="hljs-keyword">const</span> harness = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Harness</span>.<span class="hljs-title function_">open</span>(<br>    <span class="hljs-keyword">await</span> <span class="hljs-title function_">openNodeSqliteStorage</span>(<span class="hljs-string">&quot;./agent.sqlite&quot;</span>),<br>    &#123; models, registry, env &#125;,<br>    context,<br>);<br>harness.<span class="hljs-title function_">resume</span>(); <span class="hljs-comment">// continue the interrupted run</span><br><span class="hljs-comment">// 继续被中断的运行。</span><br><span class="hljs-keyword">const</span> root = <span class="hljs-keyword">await</span> harness.<span class="hljs-title function_">root</span>(context);<br><span class="hljs-comment">// the same submission, answered</span><br><span class="hljs-comment">// 同一个提交，已得到回答。</span><br><span class="hljs-keyword">const</span> settled = <span class="hljs-keyword">await</span> (<span class="hljs-keyword">await</span> root.<span class="hljs-title function_">submit</span>(job, context)).<span class="hljs-title function_">wait</span>(context);<br></code></pre></td></tr></table></figure><h2 id="many-conversations-at-once-同时运行多个对话"><a href="#many-conversations-at-once-同时运行多个对话" class="headerlink" title="Many conversations at once 同时运行多个对话"></a>Many conversations at once 同时运行多个对话</h2><p>We want one harness to run many conversations at the same time, without one blocking another.</p><p>我们希望一个运行框架能同时运行多个对话，而不会让它们互相阻塞。</p><p>In Pi Durable, one harness runs as many conversations as you need, concurrently, all with the same guarantees. A conversation starts fresh or forks another one at any point in its transcript, and sees the parent’s history up to that point without copying it.</p><p>在 Pi Durable 中，一个运行框架可以按需并发运行任意数量的对话，所有对话都享有相同的保障。对话可以从头开始，也可以在另一个对话记录中的任意位置创建分支（fork），并且无需复制，就能看到父对话截至该位置的历史。</p><p>Think of a Slack channel where your agent answers mentions from anyone. Then somebody opens a thread. The channel can be one conversation, and the thread a fork of it at the message the thread replies to. Both run at the same time and neither blocks the other.</p><p>想象一个 Slack 频道，你的智能体会回应任何人对它的提及。随后有人开启了一个讨论串。频道可以是一个对话，而讨论串则可以从它所回复的那条消息处分支出来。两个对话同时运行，互不阻塞。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">const</span> channel = <span class="hljs-keyword">await</span> harness.<span class="hljs-title function_">root</span>(context);<br><span class="hljs-keyword">const</span> question = <span class="hljs-keyword">await</span> channel.<span class="hljs-title function_">submit</span>(<br>    &#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;@agent why did the deploy fail?&quot;</span> &#125;,<br>    context,<br>);<br><span class="hljs-keyword">const</span> answered = <span class="hljs-keyword">await</span> question.<span class="hljs-title function_">wait</span>(context);<br><br><span class="hljs-comment">// Someone replies to the agent&#x27;s answer in a thread. Every conversation names</span><br><span class="hljs-comment">// its owner, which decides what an abort reaches (more on that under Tasks).</span><br><span class="hljs-comment">// The thread has none.</span><br><span class="hljs-comment">// 有人在讨论串中回复智能体的回答。每个对话都指定其所有者，</span><br><span class="hljs-comment">// 这决定了中止操作的影响范围（详见「任务」一节）。此讨论串没有所有者。</span><br><span class="hljs-keyword">const</span> thread = <span class="hljs-keyword">await</span> channel.<span class="hljs-title function_">fork</span>(<br>    answered.<span class="hljs-property">answer</span>!,<br>    &#123; <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;ownerless&quot;</span> &#125; &#125;,<br>    context,<br>);<br><br><span class="hljs-comment">// Both conversations work at the same time.</span><br><span class="hljs-comment">// 两个对话同时工作。</span><br><span class="hljs-keyword">const</span> inThread = <span class="hljs-keyword">await</span> thread.<span class="hljs-title function_">submit</span>(<br>    &#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;@agent can we roll it back?&quot;</span> &#125;,<br>    context,<br>);<br><span class="hljs-keyword">const</span> inChannel = <span class="hljs-keyword">await</span> channel.<span class="hljs-title function_">submit</span>(<br>    &#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;@agent who is on call today?&quot;</span> &#125;,<br>    context,<br>);<br><span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([inThread.<span class="hljs-title function_">wait</span>(context), inChannel.<span class="hljs-title function_">wait</span>(context)]);<br></code></pre></td></tr></table></figure><p>Each conversation also stores its own agent: the model, the thinking level, the selected extensions and which of their tools are active, extra instructions, and the working directory in its execution environment. A reviewer next to the main agent can use a cheaper model, read-only tools, and its own checkout.</p><p>每个对话还会保存自己的智能体配置：模型、思考级别、选用的扩展及其中启用的工具、额外指令，以及执行环境中的工作目录。主智能体旁边的审查智能体可以使用更便宜的模型、只读工具，以及自己独立检出的代码。</p><h2 id="extensions-扩展"><a href="#extensions-扩展" class="headerlink" title="Extensions 扩展"></a>Extensions 扩展</h2><p>We want everything an agent can do to be pluggable, and every plugged-in piece to take part in durability.</p><p>我们希望智能体的所有能力都可以通过插件接入，而且每个接入的组件都能参与持久化机制。</p><p>In Pi Durable, an extension is a named bundle of system prompt sections, tools, hooks, and tasks. The application installs extensions in a registry. Each conversation selects which extensions and tools it uses, and stores only their names.</p><p>在 Pi Durable 中，扩展是一个具名的组合，包含系统提示词片段、工具、钩子和任务。应用把扩展安装到注册表中。每个对话选择自己使用的扩展和工具，并且只保存它们的名称。</p><h3 id="system-prompt-sections-系统提示词片段"><a href="#system-prompt-sections-系统提示词片段" class="headerlink" title="System prompt sections 系统提示词片段"></a>System prompt sections 系统提示词片段</h3><p>The system prompt is rebuilt from the sections of the conversation’s extensions before every request, so a changed section is picked up by the next request. Pi Durable records what changed in the transcript, at the position where it changed, so a restart or a fork sees exactly what the model saw. On models that support system prompt and tool changes in the middle of a conversation, only the change is sent, so the prompt cache stays valid.</p><p>每次请求之前，系统提示词都会根据对话所选扩展的片段重新构建，因此片段一旦变化，下一次请求就会使用新内容。Pi Durable 会在对话记录中发生变化的位置记录这些改动，因此重启或创建分支后，看到的内容与模型当时看到的完全一致。对于支持在对话过程中更改系统提示词和工具的模型，只会发送变化的部分，所以提示词缓存仍然有效。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; defineExtension, section &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">ProjectContext</span> = <span class="hljs-title function_">defineExtension</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;project-context&quot;</span>,<br>    <span class="hljs-attr">sections</span>: [<br>        <span class="hljs-comment">// Read from the conversation&#x27;s execution environment. The files can be</span><br>        <span class="hljs-comment">// loaded and watched in the background; every request renders the</span><br>        <span class="hljs-comment">// latest state.</span><br>        <span class="hljs-comment">// 从对话的执行环境中读取。文件可以在后台加载并监听变化；每次请求都渲染最新状态。</span><br>        <span class="hljs-title function_">section</span>(<span class="hljs-string">&quot;agents_md&quot;</span>, <span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> agentsMd.<span class="hljs-title function_">latest</span>(input.<span class="hljs-property">env</span>)),<br>        <span class="hljs-title function_">section</span>(<span class="hljs-string">&quot;skills&quot;</span>, <span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> skills.<span class="hljs-title function_">latest</span>(input.<span class="hljs-property">env</span>)),<br>    ],<br>&#125;);<br></code></pre></td></tr></table></figure><h3 id="tools-工具"><a href="#tools-工具" class="headerlink" title="Tools 工具"></a>Tools 工具</h3><p>Every tool call runs as its own durable task, and its intent is stored before it runs. After a crash, a tool reruns only if it says that is safe. Otherwise the model is told the call was interrupted, with the output stored so far, and decides what to do. Each conversation can also get its own set of tools, like the Slack thread from earlier, which may search but not deploy.</p><p>每次工具调用都作为独立的持久化任务运行，而且调用意图会在执行前保存。发生崩溃后，只有工具声明可以安全重跑时，才会重新执行。否则，模型会收到调用已中断的通知，以及截至中断时已保存的输出，再决定如何处理。每个对话也可以拥有自己的工具集合，例如前面提到的 Slack 讨论串可以搜索，但不能部署。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">Type</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-ai&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; defineTool &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> searchIssues = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;search_issues&quot;</span>,<br>    <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Search the issue tracker&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">query</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>    <span class="hljs-attr">replay</span>: <span class="hljs-string">&quot;safe&quot;</span>, <span class="hljs-comment">// only reads, so a rerun after a crash is fine</span><br>    <span class="hljs-comment">// 只进行读取，因此崩溃后可以安全重跑。</span><br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api) =&gt; &#123;<br>        <span class="hljs-comment">// streamed to every client watching</span><br>        <span class="hljs-comment">// 以流式方式发送给每个正在观察的客户端。</span><br>        api.<span class="hljs-title function_">output</span>(<span class="hljs-string">`searching for <span class="hljs-subst">$&#123;args.query&#125;</span>\n`</span>);<br>        <span class="hljs-keyword">return</span> &#123;<br>            <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, <span class="hljs-attr">text</span>: <span class="hljs-keyword">await</span> tracker.<span class="hljs-title function_">search</span>(args.<span class="hljs-property">query</span>) &#125;],<br>        &#125;;<br>    &#125;,<br>&#125;);<br><br><span class="hljs-keyword">const</span> deploy = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;deploy&quot;</span>,<br>    <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Deploy a version to production&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">version</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>    <span class="hljs-comment">// No replay: a deploy interrupted by a crash is reported to the model,</span><br>    <span class="hljs-comment">// never repeated.</span><br>    <span class="hljs-comment">// 不重放：崩溃导致部署中断时，会报告给模型，绝不重复执行。</span><br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args) =&gt; (&#123;<br>        <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, <span class="hljs-attr">text</span>: <span class="hljs-keyword">await</span> ci.<span class="hljs-title function_">deploy</span>(args.<span class="hljs-property">version</span>) &#125;],<br>    &#125;),<br>&#125;);<br><br>registry.<span class="hljs-title function_">install</span>(<span class="hljs-title function_">defineExtension</span>(&#123; <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;ops&quot;</span>, <span class="hljs-attr">tools</span>: [searchIssues, deploy] &#125;));<br><br><span class="hljs-comment">// The thread may search, but not deploy.</span><br><span class="hljs-comment">// 这个讨论串可以搜索，但不能部署。</span><br><span class="hljs-keyword">await</span> thread.<span class="hljs-title function_">configure</span>(&#123; <span class="hljs-attr">tools</span>: &#123; <span class="hljs-attr">remove</span>: [deploy] &#125; &#125;, context);<br></code></pre></td></tr></table></figure><p>A tool gets the harness API for its call: it can commit entries and documents, start tasks and conversations, and talk to other conversations. That makes a subagent a few lines of code. A tool creates a conversation it owns, gives it a smaller model and its own instructions, and waits for its answer. The subagent is a conversation like any other, so it survives a crash, counts its own cost, and a UI can show it under the call.</p><p>工具会获得当前调用对应的运行框架 API：它可以提交记录条目和文档、启动任务和对话，也能与其他对话通信。因此，只需几行代码就能实现子智能体。工具创建一个归自己所有的对话，为它配置更小的模型和专门的指令，再等待回答。子智能体与其他对话一样，能够在崩溃后恢复、单独统计自己的费用，界面也可以把它展示在对应的工具调用下面。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> &#123; <span class="hljs-title class_">AssistantMessage</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-ai&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; <span class="hljs-title class_">AssistantEntry</span>, configure &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> triage = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;triage&quot;</span>,<br>    <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Label an incoming issue as bug, feature, or question&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">issue</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>    <span class="hljs-comment">// a rerun after a crash finds the same subagent and the same submission</span><br>    <span class="hljs-comment">// 崩溃后重跑，会找到同一个子智能体和同一次提交。</span><br>    <span class="hljs-attr">replay</span>: <span class="hljs-string">&quot;safe&quot;</span>,<br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>        <span class="hljs-keyword">const</span> child = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">commit</span>(<span class="hljs-title function_">async</span> (tx) =&gt; &#123;<br>            <span class="hljs-keyword">const</span> existing = (<br>                <span class="hljs-keyword">await</span> tx.<span class="hljs-title function_">scanConversations</span>(&#123; <span class="hljs-attr">ownerTaskId</span>: api.<span class="hljs-property">taskId</span> &#125;, <span class="hljs-number">1</span>)<br>            ).<span class="hljs-property">items</span>[<span class="hljs-number">0</span>];<br>            <span class="hljs-keyword">if</span> (existing !== <span class="hljs-literal">undefined</span>) <span class="hljs-keyword">return</span> existing.<span class="hljs-property">id</span>;<br>            <span class="hljs-comment">// Owned by this call, so aborting the call aborts the subagent.</span><br>            <span class="hljs-comment">// 归当前调用所有，因此中止调用也会中止子智能体。</span><br>            <span class="hljs-keyword">const</span> created = <span class="hljs-keyword">await</span> tx.<span class="hljs-title function_">createConversation</span>(&#123;<br>                <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;task&quot;</span>, <span class="hljs-attr">taskId</span>: api.<span class="hljs-property">taskId</span> &#125;,<br>            &#125;);<br>            <span class="hljs-comment">// It starts as a copy of this conversation&#x27;s agent. Make it a small</span><br>            <span class="hljs-comment">// model without tools.</span><br>            <span class="hljs-comment">// 初始配置复制自当前对话的智能体；改为不带工具的小模型。</span><br>            <span class="hljs-keyword">await</span> <span class="hljs-title function_">configure</span>(tx, created.<span class="hljs-property">id</span>, &#123;<br>                <span class="hljs-attr">model</span>: &#123; <span class="hljs-attr">provider</span>: <span class="hljs-string">&quot;openai&quot;</span>, <span class="hljs-attr">modelId</span>: <span class="hljs-string">&quot;gpt-6-luna&quot;</span> &#125;,<br>                <span class="hljs-attr">tools</span>: [],<br>                <span class="hljs-attr">instructions</span>: <span class="hljs-string">&quot;Answer with one word: bug, feature, or question.&quot;</span>,<br>            &#125;);<br>            <span class="hljs-keyword">return</span> created.<span class="hljs-property">id</span>;<br>        &#125;, context);<br>        <span class="hljs-comment">// lets a UI show the subagent under the call</span><br>        <span class="hljs-comment">// 让界面能够在当前调用下面展示子智能体。</span><br>        <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">details</span>(&#123; <span class="hljs-attr">conversationId</span>: child &#125;, context);<br>        <span class="hljs-keyword">const</span> subagent = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">conversation</span>(child, context);<br>        <span class="hljs-keyword">const</span> request = &#123;<br>            <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>,<br>            <span class="hljs-attr">content</span>: args.<span class="hljs-property">issue</span>,<br>            <span class="hljs-attr">requestId</span>: <span class="hljs-string">`triage:<span class="hljs-subst">$&#123;api.taskId&#125;</span>`</span>,<br>        &#125; <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>;<br>        <span class="hljs-keyword">const</span> settled = <span class="hljs-keyword">await</span> (<br>            <span class="hljs-keyword">await</span> subagent!.<span class="hljs-title function_">submit</span>(request, context)<br>        ).<span class="hljs-title function_">wait</span>(context);<br>        <span class="hljs-comment">// The answer is an entry in the subagent&#x27;s transcript. Read it and take</span><br>        <span class="hljs-comment">// its text.</span><br>        <span class="hljs-comment">// 回答是子智能体对话记录中的一个条目；读取条目并提取其文本。</span><br>        <span class="hljs-keyword">const</span> entry = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">commit</span>(<br>            <span class="hljs-function">(<span class="hljs-params">tx</span>) =&gt;</span> tx.<span class="hljs-title function_">entry</span>(<span class="hljs-title class_">AssistantEntry</span>, settled.<span class="hljs-property">answer</span>!),<br>            context,<br>        );<br>        <span class="hljs-keyword">const</span> message = entry?.<span class="hljs-property">model</span>?.[<span class="hljs-number">0</span>] <span class="hljs-keyword">as</span> <span class="hljs-title class_">AssistantMessage</span>;<br>        <span class="hljs-keyword">const</span> text = message.<span class="hljs-property">content</span><br>            .<span class="hljs-title function_">flatMap</span>(<span class="hljs-function">(<span class="hljs-params">content</span>) =&gt;</span> (content.<span class="hljs-property">type</span> === <span class="hljs-string">&quot;text&quot;</span> ? [content.<span class="hljs-property">text</span>] : []))<br>            .<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;&quot;</span>);<br>        <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, text &#125;] &#125;;<br>    &#125;,<br>&#125;);<br></code></pre></td></tr></table></figure><p>Extensions can also change other extensions’ tools. A tool with the same name in a later extension replaces the earlier one, for example a bash that runs inside a Python virtualenv. A wrap decorates whichever tool won, wherever the wrapping extension is selected.</p><p>扩展还可以修改其他扩展的工具。后面的扩展中，同名工具会替换前面的工具，例如替换成在 Python 虚拟环境中运行的 bash。只要选用了包含包装逻辑的扩展，包装器（wrap）就会装饰最终生效的那个工具，无论它来自哪个扩展。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; wrapTool &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><span class="hljs-keyword">import</span> &#123; createBashTool &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable/tools&quot;</span>;<br><br><span class="hljs-comment">// Times every bash call, whichever bash the conversation ends up with.</span><br><span class="hljs-comment">// 为每次 bash 调用计时，无论对话最终选用的是哪个 bash。</span><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Timing</span> = <span class="hljs-title function_">defineExtension</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;timing&quot;</span>,<br>    <span class="hljs-attr">wraps</span>: [<br>        <span class="hljs-title function_">wrapTool</span>(<span class="hljs-title function_">createBashTool</span>(), <span class="hljs-function">(<span class="hljs-params">bash</span>) =&gt;</span> (&#123;<br>            ...bash,<br>            <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>                <span class="hljs-keyword">const</span> start = <span class="hljs-title class_">Date</span>.<span class="hljs-title function_">now</span>();<br>                <span class="hljs-keyword">try</span> &#123;<br>                    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> bash.<span class="hljs-title function_">execute</span>(args, api, context);<br>                &#125; <span class="hljs-keyword">finally</span> &#123;<br>                    metrics.<span class="hljs-title function_">record</span>(<span class="hljs-string">&quot;bash&quot;</span>, <span class="hljs-title class_">Date</span>.<span class="hljs-title function_">now</span>() - start);<br>                &#125;<br>            &#125;,<br>        &#125;)),<br>    ],<br>&#125;);<br></code></pre></td></tr></table></figure><h3 id="hooks-钩子"><a href="#hooks-钩子" class="headerlink" title="Hooks 钩子"></a>Hooks 钩子</h3><p>Hooks let extensions step into tasks, including the built-in tasks for generating a model response, invoking a tool, or performing compaction. They can rewrite a request before it goes to the model, block or rewrite a tool call, replace a result, keep a run going, or write a summary themselves. A hook can run again after a crash, so a hook that makes a decision stores it in a memo: a small value stored with the task, where the first write wins.</p><p>钩子让扩展可以介入任务，包括生成模型回答、调用工具或进行上下文压缩的内置任务。它们可以在请求发给模型之前改写请求，阻止或改写工具调用，替换结果，让一次运行继续下去，也可以自行生成摘要。崩溃后钩子可能再次运行，因此需要做决策的钩子会把决定保存在 memo 中：这是与任务一起保存的一个小值，以首次写入的值为准。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; hook, <span class="hljs-title class_">ToolTask</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Approval</span> = <span class="hljs-title function_">defineExtension</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;approval&quot;</span>,<br>    <span class="hljs-attr">hooks</span>: [<br>        <span class="hljs-title function_">hook</span>(<span class="hljs-title class_">ToolTask</span>, &#123;<br>            <span class="hljs-attr">beforeTool</span>: <span class="hljs-title function_">async</span> (call, api, context) =&gt; &#123;<br>                <span class="hljs-keyword">if</span> (call.<span class="hljs-property">name</span> !== <span class="hljs-string">&quot;deploy&quot;</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;<br>                <span class="hljs-comment">// After a restart, the hook finds the stored answer instead of</span><br>                <span class="hljs-comment">// asking again.</span><br>                <span class="hljs-comment">// 重启后，钩子会找到已保存的答案，不再重复询问。</span><br>                <span class="hljs-keyword">let</span> approved = <span class="hljs-keyword">await</span> api.<span class="hljs-property">memo</span>&lt;<span class="hljs-built_in">boolean</span>&gt;(<br>                    <span class="hljs-string">&quot;approval:deploy&quot;</span>,<br>                    context,<br>                );<br>                approved ??= <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">memo</span>(<br>                    <span class="hljs-string">&quot;approval:deploy&quot;</span>,<br>                    <span class="hljs-keyword">await</span> <span class="hljs-title function_">askInSlack</span>(call),<br>                    context,<br>                );<br>                <span class="hljs-keyword">return</span> approved<br>                    ? <span class="hljs-literal">undefined</span><br>                    : &#123; <span class="hljs-attr">block</span>: <span class="hljs-string">&quot;Nobody approved the deploy.&quot;</span> &#125;;<br>            &#125;,<br>        &#125;),<br>    ],<br>&#125;);<br></code></pre></td></tr></table></figure><p>Several extensions can hook the same thing. Their hooks run as a chain, in the order the conversation selects the extensions, and each hook defines how its chain runs. <code>beforeTool</code> passes rewritten arguments down the chain, and the first block stops it. <code>afterTool</code> passes the result down the chain. <code>onYield</code> stops at the first hook that keeps the run going. Observers like <code>afterResponse</code> always run them all. A hook that throws is reported, and the chain continues, except in <code>beforeTool</code>, where a throw blocks the call.</p><p>多个扩展可以对同一处操作挂接钩子。它们按照对话选择扩展的顺序串成调用链，每种钩子都有自己的链式执行规则。<code>beforeTool</code> 把改写后的参数向下传递，遇到第一个阻止调用的结果就停止。<code>afterTool</code> 把结果沿调用链传递。<code>onYield</code> 遇到第一个决定继续运行的钩子就停止。<code>afterResponse</code> 等观察型钩子则总会全部执行。如果某个钩子抛出异常，异常会被报告，而调用链会继续执行；但 <code>beforeTool</code> 是例外，在这里抛出异常会阻止工具调用。</p><h3 id="tasks-任务"><a href="#tasks-任务" class="headerlink" title="Tasks 任务"></a>Tasks 任务</h3><p>The harness runs conversations with built-in tasks: one for each model request, one for each tool call, and one for compaction. Extensions bring their own tasks and get the same machinery: a checkpoint after every step, timers that survive restarts, and waiting on other tasks.</p><p>运行框架通过内置任务来运行对话：每个模型请求对应一个任务，每次工具调用对应一个任务，上下文压缩也对应一个任务。扩展可以提供自己的任务，并获得相同的运行机制：每一步之后的检查点、重启后仍然有效的计时器，以及等待其他任务的能力。</p><p>A checkout that splits the bill across several cards charges every card at once. If one card is declined, the other payments are aborted and refund themselves:</p><p>假设结账时把账单分摊到多张银行卡，系统会同时向每张卡扣款。如果其中一张卡被拒付，其他支付就会被中止，并各自退款：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; defineTask, <span class="hljs-keyword">type</span> <span class="hljs-title class_">TaskId</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Payment</span> = defineTask&lt;&#123; <span class="hljs-attr">card</span>: <span class="hljs-built_in">string</span> &#125;, &#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;charge&quot;</span> &#125;, <span class="hljs-built_in">string</span>&gt;(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;shop.payment&quot;</span>,<br>    <span class="hljs-attr">version</span>: <span class="hljs-number">1</span>,<br>    <span class="hljs-attr">initial</span>: <span class="hljs-function">() =&gt;</span> (&#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;charge&quot;</span> &#125;),<br>    <span class="hljs-attr">phases</span>: &#123;<br>        <span class="hljs-attr">charge</span>: <span class="hljs-title function_">async</span> (task, runtime, context) =&gt; &#123;<br>            <span class="hljs-comment">// The key makes the charge idempotent: if a crash reruns this</span><br>            <span class="hljs-comment">// phase, the card is only charged once.</span><br>            <span class="hljs-comment">// 这个键让扣款具备幂等性：即使崩溃后重跑这一阶段，也只会向这张卡扣款一次。</span><br>            <span class="hljs-keyword">const</span> charge = <span class="hljs-keyword">await</span> bank.<span class="hljs-title function_">charge</span>(<br>                task.<span class="hljs-property">input</span>.<span class="hljs-property">card</span>,<br>                <span class="hljs-string">`payment-<span class="hljs-subst">$&#123;task.id&#125;</span>`</span>,<br>            );<br>            <span class="hljs-keyword">await</span> runtime.<span class="hljs-title function_">commit</span>(<br>                <span class="hljs-function">() =&gt;</span> (&#123;<br>                    <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;terminal&quot;</span>,<br>                    <span class="hljs-attr">outcome</span>: charge.<span class="hljs-property">ok</span><br>                        ? &#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;completed&quot;</span>, <span class="hljs-attr">result</span>: charge.<span class="hljs-property">receipt</span> &#125;<br>                        : &#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;failed&quot;</span>, <span class="hljs-attr">error</span>: &#123; <span class="hljs-attr">message</span>: charge.<span class="hljs-property">error</span> &#125; &#125;,<br>                &#125;),<br>                context,<br>            );<br>        &#125;,<br>    &#125;,<br>    <span class="hljs-comment">// Another payment failed, or the checkout was cancelled: undo this one.</span><br>    <span class="hljs-comment">// 另一笔支付失败，或结账被取消：撤销这笔支付。</span><br>    <span class="hljs-attr">abort</span>: <span class="hljs-title function_">async</span> (task, runtime, context) =&gt; &#123;<br>        <span class="hljs-keyword">await</span> bank.<span class="hljs-title function_">refund</span>(<span class="hljs-string">`payment-<span class="hljs-subst">$&#123;task.id&#125;</span>`</span>);<br>        <span class="hljs-keyword">await</span> runtime.<span class="hljs-title function_">commit</span>(<br>            <span class="hljs-function">() =&gt;</span> (&#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;terminal&quot;</span>, <span class="hljs-attr">outcome</span>: &#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;aborted&quot;</span> &#125; &#125;),<br>            context,<br>        );<br>    &#125;,<br>&#125;);<br><br><span class="hljs-keyword">type</span> <span class="hljs-title class_">CheckoutState</span> =<br>    | &#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;pay&quot;</span> &#125;<br>    | &#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;decide&quot;</span>; <span class="hljs-attr">payments</span>: <span class="hljs-title class_">TaskId</span>&lt;<span class="hljs-built_in">string</span>&gt;[] &#125;;<br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Checkout</span> = defineTask&lt;&#123; <span class="hljs-attr">cards</span>: <span class="hljs-built_in">string</span>[] &#125;, <span class="hljs-title class_">CheckoutState</span>, <span class="hljs-built_in">string</span>&gt;(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;shop.checkout&quot;</span>,<br>    <span class="hljs-attr">version</span>: <span class="hljs-number">1</span>,<br>    <span class="hljs-attr">initial</span>: <span class="hljs-function">() =&gt;</span> (&#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;pay&quot;</span> &#125;),<br>    <span class="hljs-attr">phases</span>: &#123;<br>        <span class="hljs-attr">pay</span>: <span class="hljs-title function_">async</span> (task, runtime, context) =&gt; &#123;<br>            <span class="hljs-keyword">await</span> runtime.<span class="hljs-title function_">commit</span>(<span class="hljs-title function_">async</span> (tx) =&gt; &#123;<br>                <span class="hljs-keyword">const</span> <span class="hljs-attr">payments</span>: <span class="hljs-title class_">TaskId</span>&lt;<span class="hljs-built_in">string</span>&gt;[] = [];<br>                <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> card <span class="hljs-keyword">of</span> task.<span class="hljs-property">input</span>.<span class="hljs-property">cards</span>) &#123;<br>                    payments.<span class="hljs-title function_">push</span>(<br>                        <span class="hljs-keyword">await</span> tx.<span class="hljs-title function_">createTask</span>(<span class="hljs-title class_">Payment</span>, &#123; card &#125;, &#123;<br>                            <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;task&quot;</span>, <span class="hljs-attr">taskId</span>: task.<span class="hljs-property">id</span> &#125;,<br>                        &#125;),<br>                    );<br>                &#125;<br>                <span class="hljs-comment">// Run no code until every payment is done. The first failed</span><br>                <span class="hljs-comment">// payment aborts the others.</span><br>                <span class="hljs-comment">// 所有支付结束前不执行任何代码。第一笔失败的支付会中止其他支付。</span><br>                <span class="hljs-keyword">return</span> &#123;<br>                    <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;waiting&quot;</span>,<br>                    <span class="hljs-attr">checkpoint</span>: &#123; <span class="hljs-attr">phase</span>: <span class="hljs-string">&quot;decide&quot;</span>, payments &#125;,<br>                    <span class="hljs-attr">on</span>: payments,<br>                    <span class="hljs-attr">policy</span>: <span class="hljs-string">&quot;failFast&quot;</span>,<br>                &#125;;<br>            &#125;, context);<br>        &#125;,<br>        <span class="hljs-attr">decide</span>: <span class="hljs-title function_">async</span> (task, runtime, context) =&gt; &#123;<br>            <span class="hljs-keyword">const</span> outcomes = <span class="hljs-keyword">await</span> runtime.<span class="hljs-title function_">outcomes</span>(<br>                task.<span class="hljs-property">state</span>.<span class="hljs-property">checkpoint</span>.<span class="hljs-property">payments</span>,<br>                context,<br>            );<br>            <span class="hljs-keyword">const</span> paid = outcomes.<span class="hljs-title function_">every</span>(<br>                <span class="hljs-function">(<span class="hljs-params">outcome</span>) =&gt;</span> outcome.<span class="hljs-property">status</span> === <span class="hljs-string">&quot;completed&quot;</span>,<br>            );<br>            <span class="hljs-keyword">await</span> runtime.<span class="hljs-title function_">commit</span>(<br>                <span class="hljs-function">() =&gt;</span> (&#123;<br>                    <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;terminal&quot;</span>,<br>                    <span class="hljs-attr">outcome</span>: paid<br>                        ? &#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;completed&quot;</span>, <span class="hljs-attr">result</span>: <span class="hljs-string">&quot;Order placed.&quot;</span> &#125;<br>                        : &#123;<br>                            <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;failed&quot;</span>,<br>                            <span class="hljs-attr">error</span>: &#123; <span class="hljs-attr">message</span>: <span class="hljs-string">&quot;A payment failed.&quot;</span> &#125;,<br>                        &#125;,<br>                &#125;),<br>                context,<br>            );<br>        &#125;,<br>    &#125;,<br>    <span class="hljs-attr">abort</span>: <span class="hljs-function">(<span class="hljs-params">_task, runtime, context</span>) =&gt;</span><br>        runtime.<span class="hljs-title function_">commit</span>(<br>            <span class="hljs-function">() =&gt;</span> (&#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;terminal&quot;</span>, <span class="hljs-attr">outcome</span>: &#123; <span class="hljs-attr">status</span>: <span class="hljs-string">&quot;aborted&quot;</span> &#125; &#125;),<br>            context,<br>        ),<br>&#125;);<br><br><span class="hljs-comment">// The agent starts a checkout with a tool.</span><br><span class="hljs-comment">// 智能体通过工具启动结账。</span><br><span class="hljs-keyword">const</span> checkout = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;checkout&quot;</span>,<br>    <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Pay for the cart, split across several cards&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">cards</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Array</span>(<span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>()) &#125;),<br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>        <span class="hljs-comment">// Owned by this call: aborting the call aborts the checkout and refunds</span><br>        <span class="hljs-comment">// its payments.</span><br>        <span class="hljs-comment">// 归当前调用所有：中止调用也会中止结账，并退还各笔支付。</span><br>        <span class="hljs-keyword">const</span> owner = &#123;<br>            <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;task&quot;</span>, <span class="hljs-attr">taskId</span>: api.<span class="hljs-property">taskId</span> &#125;,<br>        &#125; <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>;<br>        <span class="hljs-keyword">const</span> id = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">createTask</span>(<br>            <span class="hljs-title class_">Checkout</span>,<br>            &#123; <span class="hljs-attr">cards</span>: args.<span class="hljs-property">cards</span> &#125;,<br>            owner,<br>            context,<br>        );<br>        <span class="hljs-keyword">const</span> &#123; outcome &#125; = (<span class="hljs-keyword">await</span> api.<span class="hljs-title function_">waitForTask</span>(id, context)).<span class="hljs-property">state</span>;<br>        <span class="hljs-keyword">const</span> text =<br>            outcome.<span class="hljs-property">status</span> === <span class="hljs-string">&quot;completed&quot;</span> ? outcome.<span class="hljs-property">result</span> : outcome.<span class="hljs-property">status</span>;<br>        <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, text &#125;] &#125;;<br>    &#125;,<br>&#125;);<br><br>registry.<span class="hljs-title function_">install</span>(<span class="hljs-title function_">defineExtension</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;shop&quot;</span>,<br>    <span class="hljs-attr">tools</span>: [checkout],<br>    <span class="hljs-attr">tasks</span>: [<span class="hljs-title class_">Payment</span>, <span class="hljs-title class_">Checkout</span>],<br>&#125;));<br></code></pre></td></tr></table></figure><p>Tasks and conversations form one ownership tree. Aborting a task aborts what it owns, bottom-up, so every task cleans up its own effects first, and a task only finishes once the work it owns has finished. A subagent is the same pattern: a conversation owned by the tool call that started it.</p><p>任务和对话共同组成一棵所有权树。中止某个任务时，会自底向上中止它拥有的内容，这样每个任务都会先清理自己产生的影响；只有它拥有的工作全部结束后，任务本身才会结束。子智能体也是同样的模式：它是一个归启动它的工具调用所有的对话。</p><p>Tasks are foreground by default: they are part of the conversation’s current work. The conversation is idle only once they are done, and aborting the conversation, for example when the user presses Esc, aborts them and everything they own. A background task belongs to the conversation, but not to its current work. The conversation goes idle while it runs, and an ordinary abort leaves it and everything it owns alone. That fits a subagent that should outlive the turn that started it, or a reminder that fires tomorrow. Aborting the task itself, or the conversation with <code>{ background: true }</code>, still stops it.</p><p>任务默认在前台运行，属于对话当前的工作。只有这些任务完成后，对话才会进入空闲状态；中止对话时，例如用户按下 Esc，会中止这些任务，以及它们拥有的所有内容。后台任务属于对话，但不属于对话当前的工作。它运行时，对话可以进入空闲状态，普通的中止操作不会影响它或它拥有的内容。这适合需要在启动它的轮次结束后继续运行的子智能体，或者明天才触发的提醒。直接中止这个任务，或使用 <code>{ background: true }</code> 中止对话，仍然会停止它。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// Part of the current work: Esc aborts it, and the conversation waits for it.</span><br><span class="hljs-comment">// 属于当前工作：Esc 会中止它，对话会等待它完成。</span><br><span class="hljs-keyword">await</span> api.<span class="hljs-title function_">createTask</span>(<br>    <span class="hljs-title class_">Checkout</span>,<br>    input,<br>    &#123; <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;task&quot;</span>, <span class="hljs-attr">taskId</span>: api.<span class="hljs-property">taskId</span> &#125; &#125;,<br>    context,<br>);<br><br><span class="hljs-comment">// Side work: the conversation goes idle while it runs, and Esc leaves it alone.</span><br><span class="hljs-comment">// 后台工作：运行期间对话可以空闲，Esc 不会影响它。</span><br><span class="hljs-keyword">await</span> api.<span class="hljs-title function_">createTask</span>(<br>    <span class="hljs-title class_">Reminder</span>,<br>    input,<br>    &#123; <span class="hljs-attr">ownership</span>: &#123; <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;conversation&quot;</span> &#125;, <span class="hljs-attr">background</span>: <span class="hljs-literal">true</span> &#125;,<br>    context,<br>);<br></code></pre></td></tr></table></figure><h2 id="compaction-上下文压缩"><a href="#compaction-上下文压缩" class="headerlink" title="Compaction 上下文压缩"></a>Compaction 上下文压缩</h2><p>We want long conversations to keep going without the agent stopping to summarize.</p><p>我们希望长对话能持续进行，而不必让智能体停下来生成摘要。</p><p>In Pi Durable, compaction is a task like any other, and it runs while the conversation keeps going. When the context gets close to the model’s limit, a background compaction summarizes the older messages, and the summary is placed at the next turn boundary. The conversation only waits for a summary when the next request would not fit otherwise. If the provider still rejects a request as too long, the harness compacts and retries once. You can also compact manually at any time, with your own instructions. The older messages always stay in storage.</p><p>在 Pi Durable 中，上下文压缩与其他任务一样，可以在对话继续进行时运行。当上下文接近模型限制时，后台压缩任务会为旧消息生成摘要，并在下一个轮次边界插入摘要。只有下一次请求不经过摘要就无法放入上下文窗口时，对话才会等待摘要。如果提供方仍然以请求过长为由拒绝请求，运行框架会进行压缩，并重试一次。你也可以随时手动压缩，并提供自己的指令。旧消息始终保留在存储中。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">const</span> harness = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Harness</span>.<span class="hljs-title function_">open</span>(storage, &#123;<br>    models,<br>    registry,<br>    <span class="hljs-attr">settings</span>: &#123;<br>        <span class="hljs-attr">compaction</span>: &#123;<br>            <span class="hljs-comment">// past contextWindow - reserveTokens, the next request waits for a</span><br>            <span class="hljs-comment">// summary</span><br>            <span class="hljs-comment">// 超过 contextWindow - reserveTokens 后，下一次请求会等待摘要。</span><br>            <span class="hljs-attr">reserveTokens</span>: <span class="hljs-number">16384</span>,<br>            <span class="hljs-comment">// this far before that, a summary starts in the background</span><br>            <span class="hljs-comment">// 在距离上述阈值还有这么多 token 时，开始在后台生成摘要。</span><br>            <span class="hljs-attr">backgroundTokens</span>: <span class="hljs-number">32768</span>,<br>        &#125;,<br>    &#125;,<br>&#125;, context);<br><br><span class="hljs-comment">// Manual, also while the agent is working.</span><br><span class="hljs-comment">// 手动压缩，智能体工作期间也可以执行。</span><br><span class="hljs-keyword">await</span> root.<span class="hljs-title function_">compact</span>(<span class="hljs-string">&quot;Keep the names of the failing tests&quot;</span>, context);<br></code></pre></td></tr></table></figure><p><code>reset()</code> goes further: it starts a new context, optionally from a handoff note, and a tool can ask for the same by returning <code>control: { handoff }</code>. Because nothing is deleted, a second tool can still search everything before the handoff. That is all it takes to build an agent that hands off to itself and looks things up later.</p><p><code>reset()</code> 更进一步：它启动一个新的上下文，也可以选择从一份交接说明开始。工具返回 <code>control: { handoff }</code> 时，也可以请求同样的操作。由于没有删除任何内容，另一个工具仍然可以搜索交接之前的全部历史。凭这些能力，就能构建一个把工作交接给自己、之后再查阅历史的智能体。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">const</span> handoff = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;handoff&quot;</span>,<br>    <span class="hljs-attr">description</span>:<br>        <span class="hljs-string">&quot;Start over from a handoff note. &quot;</span> +<br>        <span class="hljs-string">&quot;Older messages stay searchable with search_history.&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">note</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>        <span class="hljs-comment">// Queued behind the handoff, so it starts the next run in the new</span><br>        <span class="hljs-comment">// context.</span><br>        <span class="hljs-comment">// 排在交接之后，因此会在新上下文中启动下一次运行。</span><br>        <span class="hljs-keyword">const</span> <span class="hljs-variable language_">self</span> = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">conversation</span>(api.<span class="hljs-property">conversationId</span>, context);<br>        <span class="hljs-keyword">await</span> <span class="hljs-variable language_">self</span>!.<span class="hljs-title function_">submit</span>(<br>            &#123;<br>                <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>,<br>                <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;Continue.&quot;</span>,<br>                <span class="hljs-attr">requestId</span>: <span class="hljs-string">`handoff:<span class="hljs-subst">$&#123;api.taskId&#125;</span>`</span>,<br>            &#125;,<br>            context,<br>        );<br>        <span class="hljs-comment">// Ends this run and starts a new context from the note, like</span><br>        <span class="hljs-comment">// reset(note).</span><br>        <span class="hljs-comment">// 结束本次运行，根据交接说明启动新上下文，相当于 reset(note)。</span><br>        <span class="hljs-keyword">return</span> &#123;<br>            <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">&quot;Handing off.&quot;</span> &#125;],<br>            <span class="hljs-attr">control</span>: &#123; <span class="hljs-attr">handoff</span>: args.<span class="hljs-property">note</span> &#125;,<br>        &#125;;<br>    &#125;,<br>&#125;);<br><br><span class="hljs-keyword">const</span> searchHistory = <span class="hljs-title function_">defineTool</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;search_history&quot;</span>,<br>    <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Search older messages, including those before a handoff&quot;</span>,<br>    <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">text</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>    <span class="hljs-attr">replay</span>: <span class="hljs-string">&quot;safe&quot;</span>,<br>    <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>        <span class="hljs-comment">// Tools read records through a transaction too. One that writes</span><br>        <span class="hljs-comment">// nothing stores nothing.</span><br>        <span class="hljs-comment">// 工具也通过事务读取记录；不写入任何内容的事务不会存储任何内容。</span><br>        <span class="hljs-keyword">const</span> page = <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">commit</span>(<br>            <span class="hljs-function">(<span class="hljs-params">tx</span>) =&gt;</span> tx.<span class="hljs-title function_">scanEntries</span>(&#123; <span class="hljs-attr">conversationId</span>: api.<span class="hljs-property">conversationId</span> &#125;, <span class="hljs-number">200</span>),<br>            context,<br>        );<br>        <span class="hljs-keyword">const</span> hits = page.<span class="hljs-property">items</span>.<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span><br>            <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(entry.<span class="hljs-property">model</span> ?? []).<span class="hljs-title function_">includes</span>(args.<span class="hljs-property">text</span>),<br>        );<br>        <span class="hljs-keyword">const</span> text = hits.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span> <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(entry.<span class="hljs-property">model</span>)).<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;\n&quot;</span>);<br>        <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, text &#125;] &#125;;<br>    &#125;,<br>&#125;);<br></code></pre></td></tr></table></figure><h2 id="durable-application-state-持久化的应用状态"><a href="#durable-application-state-持久化的应用状态" class="headerlink" title="Durable application state 持久化的应用状态"></a>Durable application state 持久化的应用状态</h2><p>We want the state of the application built on the agent to be as durable as the conversation itself.</p><p>我们希望基于智能体构建的应用，其状态也能像对话本身一样持久。</p><p>In Pi Durable, application state, like a todo list, a plan, a ticket, or the sandbox a conversation runs in, lives in documents. Documents are typed JSON stored next to the transcript and changed in the same atomic commits, so the state never disagrees with the transcript that produced it. Each document says what a fork starts with: the parent’s value at the fork point, its current value, or a fresh one.</p><p>在 Pi Durable 中，待办列表、计划、工单或对话运行所在的沙箱等应用状态，都保存在文档中。文档是带类型的 JSON，与对话记录一起存储，并在同一次原子提交中修改，因此状态始终与产生它的对话记录一致。每种文档都会规定分支如何初始化：使用父对话在分支点的值、父对话当前的值，或者一个全新的值。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-keyword">import</span> &#123; defineDoc &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@earendil-works/pi-durable&quot;</span>;<br><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Todos</span> = defineDoc&lt;&#123; <span class="hljs-attr">items</span>: <span class="hljs-built_in">string</span>[] &#125;&gt;(&#123;<br>    <span class="hljs-attr">kind</span>: <span class="hljs-string">&quot;app.todos&quot;</span>,<br>    <span class="hljs-attr">version</span>: <span class="hljs-number">1</span>,<br>    <span class="hljs-attr">scope</span>: <span class="hljs-string">&quot;conversation&quot;</span>,<br>    <span class="hljs-attr">history</span>: <span class="hljs-string">&quot;rewindable&quot;</span>,<br>    <span class="hljs-attr">fork</span>: <span class="hljs-string">&quot;asOf&quot;</span>, <span class="hljs-comment">// a fork starts with the todos its parent had at the fork entry</span><br>    <span class="hljs-comment">// 分支使用父对话在分支条目处的待办列表初始化。</span><br>    <span class="hljs-attr">initial</span>: <span class="hljs-function">() =&gt;</span> (&#123; <span class="hljs-attr">items</span>: [] &#125;),<br>&#125;);<br><br><span class="hljs-keyword">const</span> <span class="hljs-title class_">Todo</span> = <span class="hljs-title function_">defineExtension</span>(&#123;<br>    <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;todo&quot;</span>,<br>    <span class="hljs-attr">tools</span>: [<br>        <span class="hljs-title function_">defineTool</span>(&#123;<br>            <span class="hljs-attr">name</span>: <span class="hljs-string">&quot;todo&quot;</span>,<br>            <span class="hljs-attr">description</span>: <span class="hljs-string">&quot;Add an item to your todo list&quot;</span>,<br>            <span class="hljs-attr">parameters</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">Object</span>(&#123; <span class="hljs-attr">item</span>: <span class="hljs-title class_">Type</span>.<span class="hljs-title class_">String</span>() &#125;),<br>            <span class="hljs-attr">execute</span>: <span class="hljs-title function_">async</span> (args, api, context) =&gt; &#123;<br>                <span class="hljs-keyword">await</span> api.<span class="hljs-title function_">commit</span>(<span class="hljs-title function_">async</span> (tx) =&gt; &#123;<br>                    <span class="hljs-keyword">const</span> todos = <span class="hljs-keyword">await</span> tx.<span class="hljs-title function_">doc</span>(<span class="hljs-title class_">Todos</span>, api.<span class="hljs-property">conversationId</span>);<br>                    todos.<span class="hljs-property">items</span>.<span class="hljs-title function_">push</span>(args.<span class="hljs-property">item</span>);<br>                &#125;, context);<br>                <span class="hljs-keyword">const</span> text = <span class="hljs-string">`Added <span class="hljs-subst">$&#123;args.item&#125;</span>`</span>;<br>                <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">content</span>: [&#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>, text &#125;] &#125;;<br>            &#125;,<br>        &#125;),<br>    ],<br>    <span class="hljs-comment">// The model sees the list before every request.</span><br>    <span class="hljs-comment">// 模型在每次请求前都会看到这个列表。</span><br>    <span class="hljs-attr">sections</span>: [<br>        <span class="hljs-title function_">section</span>(<span class="hljs-string">&quot;todos&quot;</span>, <span class="hljs-title function_">async</span> (input, context) =&gt; &#123;<br>            <span class="hljs-keyword">const</span> todos = <span class="hljs-keyword">await</span> input.<span class="hljs-property">read</span>.<span class="hljs-title function_">snapshot</span>(<br>                <span class="hljs-title class_">Todos</span>,<br>                input.<span class="hljs-property">conversationId</span>,<br>                context,<br>            );<br>            <span class="hljs-keyword">return</span> todos?.<span class="hljs-property">items</span>.<span class="hljs-title function_">join</span>(<span class="hljs-string">&quot;\n&quot;</span>) || <span class="hljs-literal">undefined</span>;<br>        &#125;),<br>    ],<br>&#125;);<br><br><span class="hljs-comment">// A UI subscribes to the committed value.</span><br><span class="hljs-comment">// 界面订阅已提交的值。</span><br><span class="hljs-keyword">const</span> todos = <span class="hljs-keyword">await</span> harness.<span class="hljs-title function_">documentState</span>(<span class="hljs-title class_">Todos</span>, channel.<span class="hljs-property">id</span>, context);<br>todos?.<span class="hljs-title function_">subscribe</span>(<span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> <span class="hljs-title function_">renderTodos</span>(value?.<span class="hljs-property">items</span> ?? []));<br></code></pre></td></tr></table></figure><h2 id="malleable-可塑性"><a href="#malleable-可塑性" class="headerlink" title="Malleable 可塑性"></a>Malleable 可塑性</h2><p>We want to change the code of a running agent without stopping it.</p><p>我们希望在不停止智能体的情况下，修改它正在运行的代码。</p><p>In Pi Durable, the registry can change while conversations run. Installing an extension under a name that is already installed replaces it in one step. A tool call that is already running finishes on the code it started with; the next call uses the new code. Conversations store extension and tool names, never code, so after a restart they pick up whatever the new process installs.</p><p>在 Pi Durable 中，对话运行期间也可以修改注册表。以已安装的名称再次安装扩展，会一次性替换原来的扩展。已经执行中的工具调用会使用启动时的代码完成；下一次调用则使用新代码。对话只保存扩展和工具的名称，从不保存代码，因此重启后会使用新进程安装的实现。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// The extension&#x27;s file changed on disk.</span><br><span class="hljs-comment">// same name &quot;ops&quot;: replaces the installed one</span><br><span class="hljs-comment">// 扩展文件在磁盘上发生了变化；名称同为 &quot;ops&quot;，因此替换已安装的扩展。</span><br>registry.<span class="hljs-title function_">install</span>(<span class="hljs-keyword">await</span> <span class="hljs-title function_">loadExtension</span>(<span class="hljs-string">&quot;./ops.ts&quot;</span>));<br></code></pre></td></tr></table></figure><h2 id="multiplayer-多人协作"><a href="#multiplayer-多人协作" class="headerlink" title="Multiplayer 多人协作"></a>Multiplayer 多人协作</h2><p>We want many people and clients to work with the same conversations at once: watch them, join late, and steer them.</p><p>我们希望多人和多个客户端能同时参与同一批对话：观察它们、稍后加入，并引导它们。</p><p>In Pi Durable, everything a UI needs is committed state, so any number of clients can attach to any conversation in the harness. A client gets the current view first: the transcript, the answer being streamed, running tools and their output, queued messages, the agent, and usage. After that it only gets what changes. A client that joins late or reconnects starts from the current view. Any client can steer a running conversation or queue a follow-up.</p><p>在 Pi Durable 中，界面所需的一切都是已提交的状态，因此任意数量的客户端都能连接到运行框架中的任意对话。客户端首先获得当前视图：对话记录、正在流式生成的回答、运行中的工具及其输出、排队的消息、智能体配置，以及用量。之后只接收发生变化的部分。稍后加入或重新连接的客户端，从当前视图开始。任何客户端都可以引导正在运行的对话，或把后续消息加入队列。</p><figure class="highlight typescript"><table><tr><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// A second client joins the thread while the agent is working.</span><br><span class="hljs-comment">// 智能体工作期间，第二个客户端加入讨论串。</span><br><span class="hljs-keyword">const</span> view = <span class="hljs-keyword">await</span> thread.<span class="hljs-title function_">viewState</span>(context);<br><span class="hljs-title function_">render</span>(view.<span class="hljs-property">value</span>);<br>view.<span class="hljs-title function_">subscribe</span>(<span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> <span class="hljs-title function_">render</span>(value));<br><br><span class="hljs-comment">// And steers it. The message joins the running work after the current tool</span><br><span class="hljs-comment">// calls.</span><br><span class="hljs-comment">// 并引导它。消息会在当前工具调用结束后加入正在进行的工作。</span><br><span class="hljs-keyword">await</span> thread.<span class="hljs-title function_">submit</span>(<br>    &#123; <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;input&quot;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">&quot;Check the staging logs first&quot;</span>, <span class="hljs-attr">whenBusy</span>: <span class="hljs-string">&quot;steer&quot;</span> &#125;,<br>    context,<br>);<br></code></pre></td></tr></table></figure><p>For a remote client, <code>thread.watch()</code> delivers the exact operations of every commit, small enough to send over a socket. If you prefer the coding agent’s familiar events, <code>watchEvents()</code> turns the commits into those, at the cost of more bytes on the wire.</p><p>对于远程客户端，<code>thread.watch()</code> 会传递每次提交的具体操作，数据量小到足以通过 socket 发送。如果你更喜欢编码智能体中熟悉的事件，<code>watchEvents()</code> 可以把这些提交转换为那种事件，代价是传输的字节数更多。</p><h2 id="try-it-试用一下"><a href="#try-it-试用一下" class="headerlink" title="Try it 试用一下"></a>Try it 试用一下</h2><p>You can try Pi Durable today. Pi Durable is experimental, and the API might still change. Point your agent at <code>packages/durable</code> in a Pi checkout, have it read the <a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/blob/main/packages/durable/README.md">README</a>, the over thirty <a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/main/packages/durable/test/examples">examples</a>, the <a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/v1.0.0/packages/coding-agent/src/experimental/durable">small coding agent on Pi Durable</a>, or this beautiful <a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/v1.0.0/packages/coding-agent/src/experimental/vacation">vacation planning agent</a>, and get building.</p><p>你现在就可以试用 Pi Durable。它仍处于实验阶段，API 也可能继续变化。让你的智能体查看 Pi 代码库中的 <code>packages/durable</code>，阅读 <a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/blob/main/packages/durable/README.md">README</a>、三十多个<a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/main/packages/durable/test/examples">示例</a>、<a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/v1.0.0/packages/coding-agent/src/experimental/durable">基于 Pi Durable 的小型编码智能体</a>，或这个漂亮的<a target="_blank" rel="noopener" href="https://github.com/earendil-works/pi/tree/v1.0.0/packages/coding-agent/src/experimental/vacation">假期规划智能体</a>，然后开始构建吧。</p><p>The vacation planner is about 1,300 lines of TypeScript, most of them the TUI. If it looks like a coding agent, that’s only because it borrows its TUI components from the Pi coding agent.</p><p>假期规划器约有 1,300 行 TypeScript 代码，其中大部分是终端用户界面（TUI）。如果它看起来像编码智能体，那只是因为它借用了 Pi 编码智能体的 TUI 组件。</p><blockquote><p>终端演示：<a target="_blank" rel="noopener" href="https://earendil.com/posts/pi-durable/#try-it">在原文中播放假期规划智能体演示</a>。以下保留原演示的章节顺序和时间点。</p></blockquote><ol><li><p><strong>0:00</strong> — A vacation planner with a TUI, built on Pi Durable.</p><p>基于 Pi Durable 构建、带有 TUI 的假期规划器。</p></li><li><p><strong>0:17.9</strong> — A subagent runs three searches in parallel, each a durable task.</p><p>子智能体并行执行三项搜索，每项都是持久化任务。</p></li><li><p><strong>0:23.9</strong> — Meanwhile, the main agent is free to chat.</p><p>与此同时，主智能体仍然可以自由聊天。</p></li><li><p><strong>0:38.6</strong> — The process dies. Weather and museums are done, trains is not.</p><p>进程终止。天气和博物馆搜索已完成，火车搜索尚未完成。</p></li><li><p><strong>0:44.9</strong> — Restarted. <code>search</code> is safe to rerun: only trains runs again.</p><p>重启后，由于 <code>search</code> 可以安全重跑，只有火车搜索重新执行。</p></li><li><p><strong>0:48.9</strong> — Switch to the subagent and steer it.</p><p>切换到子智能体，并引导它。</p></li><li><p><strong>1:02.3</strong> — Back in main: ask, compact, and steer, all while it works.</p><p>回到主智能体：在它工作的同时提问、压缩上下文并引导它。</p></li><li><p><strong>1:27</strong> — The report arrives as a message; main turns it into a plan.</p><p>报告以消息的形式送达，主智能体把它整理成计划。</p></li></ol><p>To run both demos from a Pi checkout:</p><p>要在检出的 Pi 代码库中运行这两个演示：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">git checkout v1.0.0<br>npm install &amp;&amp; npm run build<br>node packages/coding-agent/src/experimental/durable/main.ts<br>node packages/coding-agent/src/experimental/vacation/main.ts<br></code></pre></td></tr></table></figure><p>To build on Pi Durable in your own project:</p><p>要在你自己的项目中基于 Pi Durable 进行开发：</p><figure class="highlight bash"><table><tr><td class="code"><pre><code class="hljs bash">npm install @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord<br></code></pre></td></tr></table></figure><p>In the coming weeks, we will talk more about Pi Durable and show you the small agentic tools we build with it to help us work, like a Slack bot or a GitHub triage bot. We don’t want to spill the beans yet. There is more coming as we use Pi Durable ourselves, just like we use Pi.</p><p>接下来几周，我们会进一步介绍 Pi Durable，并展示用它构建、帮助我们工作的那些小型智能体工具，例如 Slack 机器人或 GitHub 问题分流机器人。我们暂时还不想提前揭晓。随着我们像使用 Pi 一样亲自使用 Pi Durable，还会有更多内容陆续推出。</p><h2 id="faq-常见问题"><a href="#faq-常见问题" class="headerlink" title="FAQ 常见问题"></a>FAQ 常见问题</h2><h3 id="why-typescript-again-为什么又选择-typescript？"><a href="#why-typescript-again-为什么又选择-typescript？" class="headerlink" title="Why TypeScript again? 为什么又选择 TypeScript？"></a>Why TypeScript again? 为什么又选择 TypeScript？</h3><p>Because it is the easiest way to bootstrap this. But as everybody knows by now, it’s very easy to port everything to Rust or assembler. We’re not ruling this out in the future, but at the moment we are focusing on TypeScript.</p><p>因为这是把它先搭建起来最容易的方式。不过，正如大家现在都知道的，把所有内容移植到 Rust 或汇编都很容易。我们不排除未来这样做，但目前仍专注于 TypeScript。</p><details><summary>Apache License 2.0 — 原文许可全文</summary><figure class="highlight text"><table><tr><td class="code"><pre><code class="hljs text">Apache License<br>Version 2.0, January 2004<br>http://www.apache.org/licenses/<br><br>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION<br><br>1. Definitions.<br><br>&quot;License&quot; shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.<br><br>&quot;Licensor&quot; shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.<br><br>&quot;Legal Entity&quot; shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, &quot;control&quot; means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.<br><br>&quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity exercising permissions granted by this License.<br><br>&quot;Source&quot; form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.<br><br>&quot;Object&quot; form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.<br><br>&quot;Work&quot; shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work.<br><br>&quot;Derivative Works&quot; shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.<br><br>&quot;Contribution&quot; shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, &quot;submitted&quot; means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as &quot;Not a Contribution.&quot;<br><br>&quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.<br><br>2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.<br><br>3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.<br><br>4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:<br><br>(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and<br><br>(b) You must cause any modified files to carry prominent notices stating that You changed the files; and<br><br>(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and<br><br>(d) If the Work includes a &quot;NOTICE&quot; text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.<br><br>You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.<br><br>5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.<br><br>6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.<br><br>7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.<br><br>8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.<br><br>9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.<br><br>END OF TERMS AND CONDITIONS<br></code></pre></td></tr></table></figure></details>]]>
      </content:encoded>
    </item>
    <item>
      <title>[翻译]Rust 1.99.0 发布公告</title>
      <link>https://yangshifu.xyz/posts/rust-199-release-notes/</link>
      <description>Rust 1.99.0 发布公告中英对照：C ABI 可变参数函数、裸指针布局信息、Box::leak 使用建议，以及新稳定的标准库 API。</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/rust/">rust</category>
      <pubDate>Wed, 14 Oct 2026 10:49:52 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://blog.rust-lang.org/2026/10/01/Rust-1.99.0/">Announcing Rust 1.99.0</a></li><li>原作者：The Rust Release Team</li><li>原文发表于：2026-10-01</li><li>原文许可：<a target="_blank" rel="noopener" href="https://github.com/rust-lang/blog.rust-lang.org#contributing">MIT &#x2F; Apache-2.0</a></li></ul></blockquote><p>The Rust team is happy to announce a new version of Rust, 1.99.0. Rust is a programming language empowering everyone to build reliable and efficient software.</p><p>Rust 团队很高兴地宣布，Rust 1.99.0 正式发布。Rust 是一门让每个人都能构建可靠、高效软件的编程语言。</p><span id="more"></span><p>If you have a previous version of Rust installed via <code>rustup</code>, you can get 1.99.0 with:</p><p>如果你此前通过 <code>rustup</code> 安装过 Rust，可以运行以下命令升级到 1.99.0：</p><figure class="highlight sh"><table><tr><td class="code"><pre><code class="hljs sh">rustup update stable<br></code></pre></td></tr></table></figure><p>If you don’t have it already, you can get <a target="_blank" rel="noopener" href="https://www.rust-lang.org/install.html"><code>rustup</code></a> from the appropriate page on our website, and check out the <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/releases.html#version-1990-2026-10-01">detailed release notes for 1.99.0</a>.</p><p>如果你还没有安装，可以在官网的安装页面获取 <a target="_blank" rel="noopener" href="https://www.rust-lang.org/install.html"><code>rustup</code></a>，并查看 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/releases.html#version-1990-2026-10-01">1.99.0 的详细发行说明</a>。</p><p>If you’d like to help us out by testing future releases, you might consider updating locally to use the beta channel (<code>rustup default beta</code>) or the nightly channel (<code>rustup default nightly</code>). Please <a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust/issues/new/choose">report</a> any bugs you might come across!</p><p>如果你愿意帮助我们测试后续版本，可以考虑在本地切换到 beta 通道（<code>rustup default beta</code>）或 nightly 通道（<code>rustup default nightly</code>）。遇到任何 bug，都请向我们<a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust/issues/new/choose">报告</a>！</p><h2 id="what’s-in-1-99-0-stable-1-99-0-稳定版有哪些更新"><a href="#what’s-in-1-99-0-stable-1-99-0-稳定版有哪些更新" class="headerlink" title="What’s in 1.99.0 stable 1.99.0 稳定版有哪些更新"></a>What’s in 1.99.0 stable 1.99.0 稳定版有哪些更新</h2><h3 id="extern-c-variadics-可变参数函数"><a href="#extern-c-variadics-可变参数函数" class="headerlink" title="extern &quot;C&quot; variadics 可变参数函数"></a><code>extern &quot;C&quot;</code> variadics 可变参数函数</h3><p>Rust 1.99.0 stabilizes defining C-ABI variadic functions with “C” and “C-unwind” ABIs. Variadic functions defined this way use a variable argument list (<code>...</code>) and accept an arbitrary number of arguments. Rust could already call externally-defined variadic functions (e.g., <code>libc::printf</code>). With Rust 1.99, these functions can now be written in Rust itself:</p><p>Rust 1.99.0 将使用 <code>&quot;C&quot;</code> 和 <code>&quot;C-unwind&quot;</code> ABI 定义 C ABI 可变参数函数的功能正式稳定化。这类函数使用可变参数列表（<code>...</code>），可以接收任意数量的参数。Rust 此前已经能够调用在外部定义的可变参数函数，例如 <code>libc::printf</code>。从 Rust 1.99 开始，这些函数也可以直接用 Rust 编写了：</p><figure class="highlight rust"><table><tr><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">/// SAFETY: must be called with (at least) 2 i32 arguments.</span><br><span class="hljs-comment">/// 安全要求：调用时必须传入至少两个 i32 类型的参数。</span><br><span class="hljs-keyword">unsafe</span> <span class="hljs-keyword">extern</span> <span class="hljs-string">&quot;C&quot;</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">sum</span>(<span class="hljs-keyword">mut</span> args: ...) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">i32</span> &#123;<br>    <span class="hljs-comment">// SAFETY: guaranteed by the caller.</span><br>    <span class="hljs-comment">// 安全性：由调用者保证。</span><br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">a</span> = <span class="hljs-keyword">unsafe</span> &#123; args.next_arg::&lt;<span class="hljs-type">i32</span>&gt;() &#125;;<br>    <span class="hljs-keyword">let</span> <span class="hljs-variable">b</span> = <span class="hljs-keyword">unsafe</span> &#123; args.next_arg::&lt;<span class="hljs-type">i32</span>&gt;() &#125;;<br>    a + b<br>&#125;<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">foo</span>() <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">i32</span> &#123;<br>    <span class="hljs-keyword">unsafe</span> &#123; <span class="hljs-title function_ invoke__">sum</span>(<span class="hljs-number">0i32</span>, <span class="hljs-number">2i32</span>) &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>The type of <code>...</code> is <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/ffi/struct.VaList.html"><code>VaList</code></a>, which is ABI-compatible with the C <code>va_list</code> type across targets. What types can be read from a <code>VaList</code> is guarded by the <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/ffi/trait.VaArgSafe.html"><code>VaArgSafe</code></a> trait.</p><p><code>...</code> 的类型是 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/ffi/struct.VaList.html"><code>VaList</code></a>，它在各个目标平台上都与 C 的 <code>va_list</code> 类型保持 ABI 兼容。哪些类型可以从 <code>VaList</code> 中读取，由 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/ffi/trait.VaArgSafe.html"><code>VaArgSafe</code></a> trait 约束。</p><p>For more details on c-variadic functions, see the <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/reference/items/functions.html#c-variadic-functions">Reference</a>. This release also stabilizes support for defining naked variadic functions with non-“C” ABIs, which must be written via inline assembly.</p><p>有关 C 可变参数函数的更多细节，请参阅 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/reference/items/functions.html#c-variadic-functions">Rust 参考手册</a>。本次发布还稳定了使用非 <code>&quot;C&quot;</code> ABI 定义裸可变参数函数（naked variadic functions）的功能，这类函数必须通过内联汇编编写。</p><h3 id="layout-information-from-raw-pointers-从裸指针获取布局信息"><a href="#layout-information-from-raw-pointers-从裸指针获取布局信息" class="headerlink" title="Layout information from raw pointers 从裸指针获取布局信息"></a>Layout information from raw pointers 从裸指针获取布局信息</h3><p>This release settles the safety requirements for retrieving the size and alignment on raw pointers to both <code>Sized</code> (trivially safe, already possible on stable) and non-<code>Sized</code> types.</p><p>本次发布明确了通过裸指针获取 <code>Sized</code> 类型和非 <code>Sized</code> 类型的大小及对齐要求时，需要满足的安全条件。对于 <code>Sized</code> 类型，这一操作本身就是安全的，而且稳定版早已支持。</p><p>This is done by stabilizing three functions:</p><p>为此，本次发布稳定了以下三个函数：</p><ul><li><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/alloc/struct.Layout.html#method.for_value_raw"><code>Layout::for_value_raw</code></a></li><li><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/mem/fn.size_of_val_raw.html"><code>mem::size_of_val_raw</code></a></li><li><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/mem/fn.align_of_val_raw.html"><code>mem::align_of_val_raw</code></a></li></ul><h3 id="recommend-against-round-trip-unleaking-after-box-leak-不建议在调用-box-leak-后再释放内存"><a href="#recommend-against-round-trip-unleaking-after-box-leak-不建议在调用-box-leak-后再释放内存" class="headerlink" title="Recommend against round-trip unleaking after Box::leak 不建议在调用 Box::leak 后再释放内存"></a>Recommend against round-trip unleaking after <code>Box::leak</code> 不建议在调用 <code>Box::leak</code> 后再释放内存</h3><p>While there are no changes to the language semantics in Rust 1.99, we have updated the documentation on <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak"><code>Box::leak</code></a> to recommend against patterns that later deallocate that memory. This was done because such code was found to have problematic interactions with current and future potential compiler optimizations, and is especially problematic with the upcoming stabilization of custom allocators. Instead, <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.into_non_null"><code>Box::into_non_null</code></a> or <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.into_raw"><code>Box::into_raw</code></a> should be preferred.</p><p>Rust 1.99 并未改变相关的语言语义，但我们更新了 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak"><code>Box::leak</code></a> 的文档，建议避免先泄漏内存、之后又将其释放的用法。原因在于，我们发现这类代码与当前的编译器优化，以及未来可能引入的优化之间存在问题；随着自定义分配器支持即将稳定，这个问题尤其突出。应优先使用 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.into_non_null"><code>Box::into_non_null</code></a> 或 <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/std/boxed/struct.Box.html#method.into_raw"><code>Box::into_raw</code></a>。</p><p>This guidance also applies to other <code>leak</code> functions in the standard library.</p><p>这一建议也适用于标准库中的其他 <code>leak</code> 函数。</p><h3 id="stabilized-apis-已稳定的-api"><a href="#stabilized-apis-已稳定的-api" class="headerlink" title="Stabilized APIs 已稳定的 API"></a>Stabilized APIs 已稳定的 API</h3><ul><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/iter/trait.IntoIterator.html#impl-IntoIterator-for-Box%3C%5BT;+N%5D,+A%3E"><code>IntoIterator</code> for <code>Box&lt;[T; N]&gt;</code></a></p><p>为 <code>Box&lt;[T; N]&gt;</code> 实现 <code>IntoIterator</code>。</p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/iter/trait.IntoIterator.html#impl-IntoIterator-for-%26Box%3C%5BT;+N%5D,+A%3E"><code>IntoIterator</code> for <code>&amp;Box&lt;[T; N]&gt;</code></a></p><p>为 <code>&amp;Box&lt;[T; N]&gt;</code> 实现 <code>IntoIterator</code>。</p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/iter/trait.IntoIterator.html#impl-IntoIterator-for-%26mut+Box%3C%5BT;+N%5D,+A%3E"><code>IntoIterator</code> for <code>&amp;mut Box&lt;[T; N]&gt;</code></a></p><p>为 <code>&amp;mut Box&lt;[T; N]&gt;</code> 实现 <code>IntoIterator</code>。</p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/collections/struct.VecDeque.html#method.retain_back"><code>VecDeque::retain_back</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/ffi/struct.VaList.html"><code>core::ffi::VaList</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/boxed/struct.Box.html#method.into_non_null"><code>Box::into_non_null</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/boxed/struct.Box.html#method.from_non_null"><code>Box::from_non_null</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#method.into_parts"><code>Vec::into_parts</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#method.from_parts"><code>Vec::from_parts</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/mem/fn.size_of_val_raw.html"><code>core::mem::size_of_val_raw</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/mem/fn.align_of_val_raw.html"><code>core::mem::align_of_val_raw</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/core/alloc/struct.Layout.html#method.for_value_raw"><code>core::alloc::Layout::for_value_raw</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/string/struct.String.html#method.from_utf8_lossy_owned"><code>String::from_utf8_lossy_owned</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/string/struct.FromUtf8Error.html#method.into_utf8_lossy"><code>string::FromUtf8Error::into_utf8_lossy</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/iter/struct.StepBy.html#impl-FusedIterator-for-StepBy%3CI%3E"><code>FusedIterator for StepBy&lt;I&gt;</code></a></p><p>为 <code>StepBy&lt;I&gt;</code> 实现 <code>FusedIterator</code>。</p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/fs/fn.set_times.html"><code>std::fs::set_times</code></a></p></li><li><p><a target="_blank" rel="noopener" href="https://doc.rust-lang.org/stable/std/fs/fn.set_times_nofollow.html"><code>std::fs::set_times_nofollow</code></a></p></li></ul><h3 id="other-changes-其他变更"><a href="#other-changes-其他变更" class="headerlink" title="Other changes 其他变更"></a>Other changes 其他变更</h3><p>Check out everything that changed in <a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust/releases/tag/1.99.0">Rust</a>, <a target="_blank" rel="noopener" href="https://doc.rust-lang.org/nightly/cargo/CHANGELOG.html#cargo-199-2026-10-01">Cargo</a>, and <a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust-clippy/blob/master/CHANGELOG.md#rust-199">Clippy</a>.</p><p>完整的变更列表，请查看 <a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust/releases/tag/1.99.0">Rust</a>、<a target="_blank" rel="noopener" href="https://doc.rust-lang.org/nightly/cargo/CHANGELOG.html#cargo-199-2026-10-01">Cargo</a> 和 <a target="_blank" rel="noopener" href="https://github.com/rust-lang/rust-clippy/blob/master/CHANGELOG.md#rust-199">Clippy</a> 的发行说明。</p><h2 id="contributors-to-1-99-0-1-99-0-的贡献者"><a href="#contributors-to-1-99-0-1-99-0-的贡献者" class="headerlink" title="Contributors to 1.99.0 1.99.0 的贡献者"></a>Contributors to 1.99.0 1.99.0 的贡献者</h2><p>Many people came together to create Rust 1.99.0. We couldn’t have done it without all of you. <a target="_blank" rel="noopener" href="https://thanks.rust-lang.org/rust/1.99.0/">Thanks!</a></p><p>许多人共同参与了 Rust 1.99.0 的开发。没有大家的贡献，我们就无法完成这个版本。<a target="_blank" rel="noopener" href="https://thanks.rust-lang.org/rust/1.99.0/">谢谢你们！</a></p><!--Original article: https://blog.rust-lang.org/2026/10/01/Rust-1.99.0/Original author: The Rust Release TeamOriginal license: https://github.com/rust-lang/blog.rust-lang.org/blob/main/LICENSE-MITPermission is hereby granted, free of charge, to anyperson obtaining a copy of this software and associateddocumentation files (the "Software"), to deal in theSoftware without restriction, including withoutlimitation the rights to use, copy, modify, merge,publish, distribute, sublicense, and/or sell copies ofthe Software, and to permit persons to whom the Softwareis furnished to do so, subject to the followingconditions:The above copyright notice and this permission noticeshall be included in all copies or substantial portionsof the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OFANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITEDTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR APARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENTSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANYCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTIONOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF ORIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHERDEALINGS IN THE SOFTWARE.-->]]>
      </content:encoded>
    </item>
    <item>
      <title>[翻译]在 Claude Code 中选择 Claude 模型和努力级别</title>
      <link>https://yangshifu.xyz/posts/claude-model-and-effort-level-in-claude-code/</link>
      <description>
        <![CDATA[<h2 id="原文信息"><a href="#原文信息" class="headerlink" title="原文信息"></a>原文信息</h2><ul>
<li><strong>作者：</strong> Lydia Hallie</li>
<li><strong>日期：</]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <pubDate>Sun, 23 Aug 2026 07:13:46 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="原文信息"><a href="#原文信息" class="headerlink" title="原文信息"></a>原文信息</h2><ul><li><strong>作者：</strong> Lydia Hallie</li><li><strong>日期：</strong> 2026年7月7日</li><li><strong>阅读时间：</strong> 5 分钟</li><li><strong>分类：</strong> Claude Code, Enterprise AI</li><li><strong>产品：</strong> Claude Code</li><li><strong>原文链接：</strong> <a target="_blank" rel="noopener" href="https://claude.com/blog/claude-model-and-effort-level-in-claude-code">Choosing a Claude Model and Effort Level in Claude Code</a></li></ul><h2 id="key-takeaways-关键要点"><a href="#key-takeaways-关键要点" class="headerlink" title="Key Takeaways 关键要点"></a>Key Takeaways 关键要点</h2><ul><li><p>Model selection chooses the set of fixed weights, or the overall capability range of the model. While models can be provided context or steered, the model’s overall knowledge base and capabilities are set.</p></li><li><p>模型选择决定了固定权重的集合，即模型的总体能力范围。虽然可以为模型提供上下文或引导，但模型的总体知识库和能力是固定的。</p></li><li><p>Effort means more than “thinking time.” It controls how much work Claude does on your request overall including the number of files read, tools used, and how many steps it takes before it checks back in with you.</p></li><li><p>努力不仅仅意味着”思考时间”。它控制 Claude 在你的请求上总体做多少工作，包括读取多少文件、使用多少工具，以及在向你确认之前采取多少步骤。</p></li><li><p>Choose smaller models for more routine tasks and larger models for more complex or ambiguous tasks. Start with default effort levels for each model and tune as a general preference based on the type of work you do rather than task-by-task.</p></li><li><p>对于常规任务选择更小的模型，对于更复杂或模糊的任务选择更大的模型。从每个模型的默认努力级别开始，根据你做的工作类型作为通用偏好来调整，而不是逐个任务决定。</p></li><li><p>If Claude has all the pertinent context, clearly tried, and still got it wrong, that’s a signal to pick a more capable model. If Claude got it wrong by skipping a file, not running the tests, or bailing on a refactor partway through, pick a higher effort level.</p></li><li><p>如果 Claude 拥有了所有相关上下文且明显尝试过但仍然出错，这是选择更有能力模型的信号。如果 Claude 因为跳过文件、没有运行测试或在重构中途放弃而出错，选择更高的努力级别。</p></li></ul><h2 id="claude-code-effort-level-and-model-selection-claude-code-努力级别和模型选择"><a href="#claude-code-effort-level-and-model-selection-claude-code-努力级别和模型选择" class="headerlink" title="Claude Code Effort Level and Model Selection Claude Code 努力级别和模型选择"></a>Claude Code Effort Level and Model Selection Claude Code 努力级别和模型选择</h2><p>Claude Code gives you two settings that appear to “make the answer better”: the model setting and the effort level. You may expect that larger models like Claude Fable 5 provide a smarter output than Claude Sonnet, and a higher effort level means Claude thinks longer before it answers.</p><p>Claude Code 给你两个看起来”让答案更好”的设置：模型设置和努力级别。你可能期望像 Claude Fable 5 这样更大的模型比 Claude Sonnet 提供更聪明的输出，而更高的努力级别意味着 Claude 在回答前思考更长时间。</p><p>The first assumption is accurate. Our largest models are more capable, according to industry-standard benchmarks.</p><p>第一个假设是准确的。根据行业标准基准，我们最大的模型更有能力。</p><p>But effort means more than just “thinking time.” Effort level controls how much work Claude does on your request overall. This does include how long the model thinks, but also:</p><p>但努力不仅仅意味着”思考时间”。努力级别控制 Claude 在你的请求上总体做多少工作。这确实包括模型思考多长时间，但也包括：</p><p>How many files it reads;</p><p>读取多少文件；</p><p>How much it verifies; and</p><p>验证多少；以及</p><p>How far it pushes through a multi-step task before checking in with you.</p><p>在向你确认之前能推进多步骤任务多远。</p><p>At a higher effort, Claude will take more of those actions (for example, read files, run tests, and double-check) before it comes back to you. At lower effort, it would rather ask you for more context than spend tokens figuring something out on its own.</p><p>在更高的努力级别下，Claude 会在返回之前采取更多这些操作（例如，读取文件、运行测试和双重检查）。在更低的努力级别下，它宁愿请求你提供更多上下文，而不是花费 token 自己弄清楚。</p><h2 id="how-model-selection-works-模型选择如何工作"><a href="#how-model-selection-works-模型选择如何工作" class="headerlink" title="How Model Selection Works 模型选择如何工作"></a>How Model Selection Works 模型选择如何工作</h2><p>When you press enter, Claude Code assembles your message together with the system prompt, tool definitions, your CLAUDE.md, the conversation history, and any files in context. All of this is sent as one request to the API.</p><p>当你按 Enter 时，Claude Code 将你的消息与系统提示、工具定义、你的 CLAUDE.md、对话历史以及上下文中的任何文件组装在一起。所有这些都作为一次请求发送到 API。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/9ee6eb831eb26c9aa68e9ca780b60ab7.png" alt="API request assembly" loading="lazy"></p><p>Everything Claude Code has gets packed into one API request.</p><p>Claude Code 拥有的所有东西都被打包成一次 API 请求。</p><p>On the server, the text is tokenized before it ever reaches the model. The model never sees that as plain text, though. The first thing that happens on the server is tokenization: the text is split into pieces, and each piece is mapped to an integer from a fixed vocabulary the model was trained with. <code>const</code> might map to 1978, <code>await</code> might map to 4293.</p><p>在服务器上，文本在到达模型之前就被分词了。模型永远不会看到纯文本。服务器上发生的第一件事是分词：文本被分割成片段，每个片段被映射到模型训练时使用的固定词汇表中的一个整数。<code>const</code> 可能映射到 1978，<code>await</code> 可能映射到 4293。</p><p>From here on, your prompt is an array of integers.</p><p>从现在开始，你的提示是一串整数数组。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/0eefc4b5bebe1e4bf22ffa3f41353b79.png" alt="Tokenizer diagram" loading="lazy"></p><p>The tokenizer splits your text into pieces and maps each piece to an integer in a fixed vocabulary. Each chunk in the top row becomes its token ID (bottom row); IDs shown are illustrative.</p><p>分词器将你的文本分割成片段，并将每个片段映射到固定词汇表中的一个整数。顶行的每个块变成其 token ID（底行）；显示的 ID 是说明性的。</p><p>The model’s job is to take that array and predict which token comes next. It does this by computing a probability for every token in its vocabulary and picking from the top.</p><p>模型的工作是获取该数组并预测下一个 token 是什么。它通过计算其词汇表中每个 token 的概率并从最高概率中选择来做到这一点。</p><p>After <code>const x = await</code>, a well-trained model puts high probability on <code>fetch</code> (very likely) and near-zero on <code>banana</code> (not likely at all).</p><p>在 <code>const x = await</code> 之后，训练良好的模型会对 <code>fetch</code> 给予高概率（非常可能）而对 <code>banana</code> 给予接近零的概率（完全不可能）。</p><p>The model’s prediction is a probability for every token in its vocabulary. The gap between the top guess and an unrelated one is enormous.</p><p>模型的预测是其词汇表中每个 token 的概率。最高猜测与无关猜测之间的差距是巨大的。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/2899ac0fbe3a307607fa6f2f109e44b6.png" alt="Probability prediction" loading="lazy"></p><p>What turns your input tokens into those probabilities is the weights (also called parameters). These are billions of numbers organized into large matrices.</p><p>将输入 token 转化为这些概率的是权重（也称为参数）。这些是数十亿个数字，组织成大型矩阵。</p><p>To predict one token, the model runs your input through those matrices, a long chain of matrix multiplications, and reads the probabilities at the end.</p><p>为了预测一个 token，模型通过这些矩阵运行你的输入，一长串矩阵乘法，然后在末尾读取概率。</p><p>The weights are where everything the model “knows” lives.</p><p>权重是模型”知道”的所有东西的所在。</p><p>The weights of each model are set during training, and by the time you’re sending requests they’re read-only. Nothing in your prompt, your CLAUDE.md, or your context changes them.</p><p>每个模型的权重在训练期间设置，当你发送请求时它们已是只读的。你的提示、你的 CLAUDE.md 或你的上下文中的任何内容都不会改变它们。</p><p>(If you’ve run into the word inference, that’s all it means: using the model after training is done, with the weights fixed.)</p><p>（如果你遇到过”推理”这个词，这就是它的全部含义：在训练完成后使用模型，权重是固定的。）</p><p>Your prompt goes in, probabilities come out. The weights in the middle don’t change.</p><p>你的提示进去，概率出来。中间的权重不变。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/a3cc7ea9327554143745e31036e1ac0d.png" alt="Weights visualization" loading="lazy"></p><p>Everything Claude knows about TypeScript, popular frameworks, idiomatic Go, or any other general programming knowledge, was encoded into those weights at training time.</p><p>Claude 关于 TypeScript、流行框架、地道的 Go 或任何其他一般编程知识的一切都是在训练时编码到这些权重中的。</p><p>Your prompt and context can still steer the prediction (putting your real code in front of Claude is steering, and it works really well), but they don’t add anything to the weights themselves.</p><p>你的提示和上下文仍然可以引导预测（把你的真实代码放在 Claude 面前是引导，而且效果很好），但它们不会给权重本身增加任何东西。</p><p>If a library didn’t exist when the model was trained, it isn’t in the weights. You can put the docs in context and Claude will use them, but that’s steering, not teaching. Claude’s response will only be influenced for that request; the underlying model hasn’t retained the information.</p><p>如果一个库在模型训练时不存在，它就不会在权重中。你可以把文档放在上下文中，Claude 会使用它们，但那是引导，不是教学。Claude 的响应只会对该请求产生影响；底层模型并没有保留这些信息。</p><p>So when Claude confidently calls an API that doesn’t exist (a hallucination), that’s the weights producing a token sequence that looks plausible from training patterns, not a failed lookup.</p><p>所以当 Claude 自信地调用一个不存在的 API 时（幻觉），那是权重产生的一个从训练模式看起来合理的 token 序列，而不是失败的查找。</p><p>So what does changing the model actually do? It swaps which set of frozen weights handles your request.</p><p>那么改变模型实际上做什么？它切换哪组冻结权重来处理你的请求。</p><p>The model doesn’t generate a whole answer at once. It predicts one token, appends it to the sequence, and runs the whole computation again to get the next one. A 200-token response is 200 separate passes through the weights. This loop is where most of your wait time and your output cost come from.</p><p>模型不会一次生成整个答案。它预测一个 token，将其追加到序列中，然后再次运行整个计算以获得下一个。200 个 token 的响应是 200 次单独的权重传递。这个循环是你大部分等待时间和输出成本的原因。</p><p>The sequence grows by exactly one token per step. The model re-reads the whole array each time to predict what comes next.</p><p>序列每步恰好增长一个 token。模型每次都重新读取整个数组来预测下一个是什么。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/490f397a37e71382a506d9186b132626.png" alt="Token sequence growth" loading="lazy"></p><p>So the model setting decides which weights handle your request, and it also decides what each output token costs.</p><p>所以模型设置决定哪些权重处理你的请求，也决定每个输出 token 的成本。</p><p>What it doesn’t decide is how many tokens get generated. That number can vary a lot for the same prompt, depending on how much work Claude decides to do.</p><p>它不决定生成多少 token。这个数字对于相同的提示可能会有很大差异，取决于 Claude 决定做多少工作。</p><p>This is what effort level controls: how much work Claude decides to do for each turn.</p><p>这正是努力级别控制的：Claude 决定在每个回合做多少工作。</p><h2 id="how-claude-code-effort-level-works-claude-code-努力级别如何工作"><a href="#how-claude-code-effort-level-works-claude-code-努力级别如何工作" class="headerlink" title="How Claude Code Effort Level Works Claude Code 努力级别如何工作"></a>How Claude Code Effort Level Works Claude Code 努力级别如何工作</h2><p>When Claude Code is working on a task, the tokens it generates fall into a few categories:</p><p>当 Claude Code 处理任务时，它生成的 token 分为几类：</p><p>Thinking: the reasoning you see streaming before and between actions.</p><p>思考：你看到在操作之前和之间流式输出的推理。</p><p>Tool calls: structured blocks naming a tool like Read or Edit and its arguments, which Claude Code then parses and executes.</p><p>工具调用：命名工具（如 Read 或 Edit）及其参数的结构化块，然后 Claude Code 解析并执行它们。</p><p>Text to you: the plan, progress updates, the summary at the end.</p><p>给你的文本：计划、进度更新和最后的总结。</p><p>These are all ordinary output tokens from the same loop, billed at the same rate. For example, thinking tokens are generated exactly like the other output tokens and stay in context for the rest of that turn.</p><p>这些都是同一循环中的普通输出 token，按相同费率计费。例如，思考 token 与其他输出 token 的生成方式完全相同，并在该回合的剩余时间内保留在上下文中。</p><p>When Claude moves on to writing code, its earlier reasoning is part of the input just like a file it’s read.</p><p>当 Claude 继续编写代码时，它之前的推理作为输入的一部分，就像它读取的文件一样。</p><p>All of Claude’s output is tokens. Thinking, tool calls, and text to you are all generated from the same loop.</p><p>Claude 的所有输出都是 token。思考、工具调用和给你的文本都是从同一个循环生成的。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/b3fec258f4a77224e7788ae54e2422d8.png" alt="Output token categories" loading="lazy"></p><p>How does effort change any of this? The effort level is sent to the model as part of the request, right alongside your prompt. The model was trained to understand how to behave at each effort level and that learned behavior is baked into the frozen weights.</p><p>努力如何改变这些？努力级别作为请求的一部分与你的提示一起发送给模型。模型被训练来理解在每个努力级别下如何行为，那种学习到的行为被烘焙到冻结权重中。</p><p>When your request arrives, effort level is one more input the model responds to, the same way it responds to your prompt text. This sets Claude’s behavior for how thorough and certain it needs to be before it considers the task done.</p><p>当你的请求到达时，努力级别是模型响应的另一个输入，就像它响应你的提示文本一样。这设定了 Claude 的行为：它需要多么彻底和确定才能认为任务完成。</p><p>This is considered on every turn and results in more tokens to produce higher confidence answers.</p><p>每个回合都会考虑这一点，并生成更多 token 以产生更高置信度的答案。</p><p>Same prompt, two effort levels. The high effort path generates roughly 7x more tokens to reach a higher confidence answer.</p><p>相同的提示，两个努力级别。高努力路径生成大约 7 倍的 token 以达到更高置信度的答案。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/1c2c7519cc625e68cf814a4b03f88b46.png" alt="Same prompt, two effort levels" loading="lazy"></p><p>At higher effort levels, Claude often starts with creating a plan and the level of effort influences the depth and breadth of that plan. However, the plan is not frozen in place. As Claude receives results from its actions, it updates the progress that has been made and how certain it is of the accumulated result.</p><p>在更高的努力级别下，Claude 通常从创建计划开始，努力级别影响该计划的深度和广度。然而，计划不是一成不变的。当 Claude 收到其操作的结果时，它会更新已取得的进展以及它对累积结果的确定程度。</p><p>So when step 1 of a three-hypothesis debugging plan finds the bug, “investigate hypotheses 2 and 3” may no longer be necessary actions. Claude will typically say this explicitly, “the first check found it, so the remaining checks aren’t needed” and skip ahead. You see this happen in Claude Code when task lists get revised mid-run.</p><p>所以当三假设调试计划的第一步找到 bug 时，”调查假设 2 和 3”可能不再是需要操作。Claude 通常会明确说明这一点，”第一次检查找到了，所以剩余的检查不需要”然后跳过。你会在 Claude Code 中看到这种情况发生，当任务列表在运行中途被修改时。</p><p>Claude will be more predisposed to double-checking additional hypotheses or verifying correctness at higher effort levels, but it generally won’t artificially inflate usage for simple tasks at higher effort levels. In fact, our team pays close attention to “overthinking” during model training as it degrades effectiveness.</p><p>在更高的努力级别下，Claude 更倾向于双重检查其他假设或验证正确性，但通常不会在更高努力级别下对简单任务人为增加用量。事实上，我们的团队在模型训练期间非常关注”过度思考”，因为它会降低效率。</p><h2 id="picking-an-effort-level-选择努力级别"><a href="#picking-an-effort-level-选择努力级别" class="headerlink" title="Picking an Effort Level 选择努力级别"></a>Picking an Effort Level 选择努力级别</h2><p>Our guidance is that for most tasks you should use the model’s default effort level. The default is the level where Claude will scale its token usage according to what most people would want to spend on a task.</p><p>我们的指导是，对于大多数任务，你应该使用模型的默认努力级别。默认级别是 Claude 根据大多数人在任务上愿意花费的来调整 token 使用量的级别。</p><p>Think of effort as a manual override to scale how hard and long Claude works. Choose it deliberately when you have a strong preference for thoroughness or speed based on your domain or the type of work you do. Consider this more as a general preference than a task-by-task decision.</p><p>把努力想象成一个手动覆盖，用来调整 Claude 工作的难度和时长。当你对彻底性或速度有强烈偏好时，有意识地选择它，根据你的领域或你做的工作类型。把这更多地看作一种通用偏好，而不是逐个任务的决定。</p><p>Some practical insight that may help guide you following the launch of Claude Opus 4.8: in our testing we found when you use the default effort setting for Opus 4.8, it will produce better results for about the same number of tokens when compared to using the default effort setting of Opus 4.7 for the same task.</p><p>一些在 Claude Opus 4.8 发布后可能帮助指导你的实践见解：我们的测试发现，当你使用 Opus 4.8 的默认努力设置时，与对同一任务使用 Opus 4.7 的默认努力设置相比，它会产生更好的结果，而 token 数量大致相同。</p><h2 id="what-to-change-when-claude-gets-it-wrong-当-claude-出错时改变什么"><a href="#what-to-change-when-claude-gets-it-wrong-当-claude-出错时改变什么" class="headerlink" title="What to Change When Claude Gets It Wrong 当 Claude 出错时改变什么"></a>What to Change When Claude Gets It Wrong 当 Claude 出错时改变什么</h2><p>When Claude gets something wrong, your first instinct shouldn’t be to adjust a knob, but to examine the context you have provided. Is your prompt too vague? Is Claude connected to the right tools? Equipped with the right skills?</p><p>当 Claude 出错时，你的第一个本能不应该是调整旋钮，而是检查你提供的上下文。你的提示太模糊了吗？Claude 连接了正确的工具吗？配备了正确的技能吗？</p><p>If you’re increasing effort on a task that shouldn’t need it, the fix is often upstream, in your context, your CLAUDE.md, or how the task is scoped.</p><p>如果你在一个不应该需要它的任务上增加努力，修复通常在上游，在你的上下文、你的 CLAUDE.md 中，或者在任务的范围划分方式中。</p><p>But assuming you have provided clear context and Claude still gets something wrong, the question to ask yourself is: did it not try hard enough, or did it not know enough?</p><p>但假设你已经提供了清晰的上下文但 Claude 仍然出错，问自己这个问题：它是不够努力，还是不够了解？</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/8d792d2d2c9cefcffeb407c4ce72a829.png" alt="Two questions heuristic" loading="lazy"></p><p>Two questions, one fallback. Use the heuristic to pick a starting point, not a hard rule.</p><p>两个问题，一个后备。使用这个启发式方法选择起点，而不是硬性规则。</p><h3 id="model-the-problem-was-too-hard-模型：问题太难了"><a href="#model-the-problem-was-too-hard-模型：问题太难了" class="headerlink" title="Model: The Problem Was Too Hard 模型：问题太难了"></a>Model: The Problem Was Too Hard 模型：问题太难了</h3><p>Pick a larger model when the problem is genuinely hard. For example, problems like subtle bugs, unfamiliar domains, or architecture decisions. A larger model is helpful for situations where the smaller model is confidently wrong no matter how much context you give it.</p><p>当问题确实困难时选择更大的模型。例如，微妙的 bug、不熟悉的领域或架构决策等问题。更大的模型对于较小模型无论你给多少上下文都自信地出错的情况很有帮助。</p><p>Larger models are also better at handling ambiguity, whereas specific instructions directing execution are a better recipe for success on the smaller models.</p><p>更大的模型也更好地处理歧义，而引导执行的具体指令是较小模型成功的更好方法。</p><p>Pick a smaller model when the work is routine. For example, edits you can describe precisely, mechanical changes, or questions about code that’s already in context. There’s no reason to pay for capability the task doesn’t need.</p><p>当工作是常规的时候选择更小的模型。例如，你可以精确描述的编辑、机械性的更改，或关于上下文中已有代码的问题。没有理由为任务不需要的能力付费。</p><p>If Claude has all the pertinent context and clearly tried and still got it wrong, that’s a signal to pick a larger model.</p><p>如果 Claude 拥有了所有相关上下文且明显尝试过但仍然出错，这是选择更大模型的信号。</p><p>If you’re on the larger model and the work has been routine for a while, dropping down will increase speed and typically reduce cost without impacting the quality of the output.</p><p>如果你在更大的模型上且工作已经常规一段时间了，降下来会提高速度，通常降低成本而不影响输出质量。</p><h3 id="effort-claude-didn’t-try-hard-enough-努力：claude-不够努力"><a href="#effort-claude-didn’t-try-hard-enough-努力：claude-不够努力" class="headerlink" title="Effort: Claude Didn’t Try Hard Enough 努力：Claude 不够努力"></a>Effort: Claude Didn’t Try Hard Enough 努力：Claude 不够努力</h3><p>Pick a higher effort level if Claude got it wrong by skipping a file, not running the tests, or not double-checking its work. This is most relevant if you selected an effort level below the model’s default.</p><p>如果 Claude 因为跳过文件、没有运行测试或没有双重检查工作而出错，选择更高的努力级别。如果你在模型的默认设置之下选择了努力级别，这最相关。</p><h2 id="fable-vs-opus-vs-sonnet-the-specialist-the-expert-and-the-generalist-fable-vs-opus-vs-sonnet：专家、通才和通才"><a href="#fable-vs-opus-vs-sonnet-the-specialist-the-expert-and-the-generalist-fable-vs-opus-vs-sonnet：专家、通才和通才" class="headerlink" title="Fable vs. Opus vs. Sonnet: The Specialist, the Expert, and the Generalist Fable vs. Opus vs. Sonnet：专家、通才和通才"></a>Fable vs. Opus vs. Sonnet: The Specialist, the Expert, and the Generalist Fable vs. Opus vs. Sonnet：专家、通才和通才</h2><p>One way I like to think about how the two settings relate: Fable is a specialist who’s seen problems almost no one else has, Opus is the expert, and Sonnet is a really good generalist. The effort level decides how much time any of them spends on your task.</p><p>我喜欢思考这两个设置如何相关的一种方式是：Fable 是一个见过几乎无人见过的问题的专家，Opus 是专家，Sonnet 是一个非常出色的通才。努力级别决定他们中任何一个在你的任务上花费多少时间。</p><p>Opus at low effort is like getting five minutes with an expert who has deep experience with problems like yours. They bring knowledge that isn’t anywhere in your codebase: patterns they’ve seen before, gotchas they know to check for, the kind of thing you only get from having solved a lot of similar problems. But just giving them five minutes means a quick read of your code, not a careful one.</p><p>低努力级别的 Opus 就像与一位对你的类似问题有深厚经验的专家交流五分钟。他们带来的是你代码库中任何地方都没有的知识：他们以前见过的模式、他们知道要检查的陷阱，只有从解决了很多类似问题才能获得的这种东西。但只给他们五分钟意味着快速阅读你的代码，而不是仔细阅读。</p><p>Sonnet at high effort is like giving a really good generalist the whole afternoon. They’ll read everything, run things, double-check their work, and end up understanding your specific code thoroughly. What they bring less of is that “I’ve seen exactly this before” recognition.</p><p>高努力级别的 Sonnet 就像给一个非常出色的通才整个下午。他们会阅读一切、运行各种东西，双重检查工作，最终彻底理解你的具体代码。他们较少带来的是那种”我以前见过完全相同的”的识别。</p><p>Fable, even at low effort, is that specialist glancing at the problem everyone else is stuck on and still spotting the thing no one else would. That recognition is what you’re paying the most for, so it’s worth saving for the tasks that genuinely need it.</p><p>Fable，即使在低努力级别下，就是那个瞥一眼其他人都在纠结的问题却仍然发现没人会注意到的东西的专家。那种识别是你付出最多的，所以值得为真正需要它的任务保留。</p><p>None of these is universally better. The model setting is roughly how capable; the effort setting is roughly how thorough. Most real tasks need some of both.</p><p>这些都没有普遍更好的。模型设置大致是能力水平；努力设置大致是彻底程度。大多数实际任务都需要两者的一些。</p><h2 id="effort-model-and-token-consumption-努力、模型和-token-消耗"><a href="#effort-model-and-token-consumption-努力、模型和-token-消耗" class="headerlink" title="Effort, Model, and Token Consumption 努力、模型和 Token 消耗"></a>Effort, Model, and Token Consumption 努力、模型和 Token 消耗</h2><p>So how do model selection, effort, and token consumption all interact? It depends on the task.</p><p>那么模型选择、努力和 token 消耗如何相互作用？这取决于任务。</p><p>On routine work at the same effort level, both models generally will get it right. The larger model consumes more tokens with extra verification steps at a higher per-token price. That’s why dropping to the smaller model for routine stretches saves real money at no quality cost.</p><p>在相同努力级别下处理常规工作，两个模型通常都能正确完成。更大的模型因额外的验证步骤消耗更多 token，每个 token 的价格更高。这就是为什么在常规工作中降级到更小的模型可以节省真钱而没有质量成本。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/709d3d566bbbbde5a9e5e320ee3a9e6d.png" alt="Routine work comparison" loading="lazy"></p><p>Curves are for illustration purposes only, shown for a single task simple enough to be accomplished quickly by both models. They do not represent real benchmark data.</p><p>曲线仅用于说明目的，显示的是足够简单以至于两个模型都能快速完成的单个任务。它们不代表真实的基准数据。</p><p>On harder, multi-step work, the equation is different. The smaller model has to grind toward the limit of its ability, burning iterations, while the larger model reaches the same quality bar in fewer steps.</p><p>在更难的多步骤工作上，情况不同。更小的模型必须在其能力极限边缘努力，消耗迭代次数，而更大的模型以更少的步骤达到相同的质量标准。</p><p>You’re paying more per token for the larger model, but on tasks that genuinely stretch the smaller one, the total cost per task can come out lower. Also, more importantly, the larger model can accomplish tasks the smaller one cannot even at the highest effort settings.</p><p>你为更大的模型支付更高的每个 token 价格，但在真正考验更小模型的任务上，每个任务的总成本可能更低。同样，更重要的是，更大的模型可以完成更小的模型即使在最高努力级别下也无法完成的任务。</p><p>This is most pronounced with Fable. On long, multi-step work it pulls furthest ahead. In our testing, it finished jobs Opus and Sonnet can’t reach at any effort level. It also costs the most per token, which is the other reason to save it for the work that needs it.</p><p>这在 Fable 上最为明显。在长期的多步骤工作中，它远远领先。在我们的测试中，它完成了 Opus 和 Sonnet 在任何努力级别下都无法完成的工作。它也是每个 token 价格最高的，这是另一个将它保留给需要它的工作的原因。</p><p><img src="/posts/claude-model-and-effort-level-in-claude-code/images/4f90f18bb7fc586ccfc57a57a7d0c08b.png" alt="Hard multi-step work comparison" loading="lazy"></p><p>Curves are for illustration purposes only, shown for a single task hard enough to stretch both models. They do not represent real benchmark data.</p><p>曲线仅用于说明目的，显示的是足以考验两个模型的单个任务。它们不代表真实的基准数据。</p><p>The key point in the graphs above is that effort level picks how far Claude is willing to travel along the curve, but again, that doesn’t mean Claude will need to travel that far to complete the task.</p><p>上图中的关键点是努力级别选择 Claude 愿意沿着曲线走多远，但同样，这不意味着 Claude 需要走那么远才能完成任务。</p><p>Another nuance to this: effort shapes token consumption but doesn’t limit it. The only hard cap in the system is max_tokens, which truncates a response mid-stream when hit. It’s a blunt instrument, mostly relevant to API developers.</p><p>另一个细微差别：努力影响 token 消耗但不限制它。系统中唯一的硬上限是 max_tokens，它在达到时在流中截断响应。这是一个粗鲁的工具，主要与 API 开发者相关。</p><p>Softer controls, like task budgets or asking Claude to keep it brief in your prompt, are more helpful tools. They serve as guidance the model is trained to follow—it will look to conclude its tasks if it gets near the limit–rather than a wall it runs into.</p><p>更柔和的控制，如任务预算或在提示中要求 Claude 保持简短，是更有帮助的工具。它们作为模型被训练遵循的指导——如果接近限制，它会寻找结束任务的方式——而不是它撞上的墙。</p><h2 id="start-with-the-defaults-then-reach-for-the-dials-从默认设置开始，然后使用调价器"><a href="#start-with-the-defaults-then-reach-for-the-dials-从默认设置开始，然后使用调价器" class="headerlink" title="Start with the Defaults, Then Reach for the Dials 从默认设置开始，然后使用调价器"></a>Start with the Defaults, Then Reach for the Dials 从默认设置开始，然后使用调价器</h2><p>Most of the time, you shouldn’t be thinking about either setting. When a result misses the mark, ask, “did Claude not know enough or did it not try hard enough?” and adjust as needed.</p><p>大多数时候，你不应该考虑这两个设置。当结果偏离目标时，问：”Claude 是不够了解还是不够努力？”然后根据需要调整。</p><p>For the full set of techniques to increase efficiency specifically, see <a href="../maximizing-the-value-of-your-claude-code-sessions/">Maximizing the value of your Claude Code sessions</a>.</p><p>有关专门提高效率的完整技术集合，请参阅<a href="../maximizing-the-value-of-your-claude-code-sessions/">最大化你的 Claude Code 会话的价值</a>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>[翻译]最大化 Claude Code 会话的价值</title>
      <link>https://yangshifu.xyz/posts/maximizing-the-value-of-your-claude-code-sessions/</link>
      <description>
        <![CDATA[<h2 id="原文信息"><a href="#原文信息" class="headerlink" title="原文信息"></a>原文信息</h2><ul>
<li><strong>作者：</strong> Lydia Hallie</li>
<li><strong>日期：</]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/personal/">personal</category>
      <pubDate>Sat, 22 Aug 2026 22:14:03 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="原文信息"><a href="#原文信息" class="headerlink" title="原文信息"></a>原文信息</h2><ul><li><strong>作者：</strong> Lydia Hallie</li><li><strong>日期：</strong> 2026年8月14日</li><li><strong>阅读时间：</strong> 5 分钟</li><li><strong>分类：</strong> Claude Code, Enterprise AI</li><li><strong>产品：</strong> Claude Code</li><li><strong>原文链接：</strong><a target="_blank" rel="noopener" href="https://claude.com/blog/maximizing-the-value-of-your-claude-code-sessions">Maximizing the value of your Claude Code sessions</a></li></ul><h2 id="tl-dr"><a href="#tl-dr" class="headerlink" title="TL;DR"></a>TL;DR</h2><ul><li><strong>Run <code>/clear</code></strong> between tasks to prevent irrelevant context from accumulating</li><li><strong>在任务之间运行 <code>/clear</code></strong> 以防止不相关的上下文累积</li><li><strong>Set model and effort level before starting</strong> to avoid busting the prompt cache mid-conversation</li><li><strong>在开始之前设置模型和努力级别</strong> 以避免在对话中途破坏提示缓存</li><li><strong>@-mention files</strong> instead of naming them to save Read calls</li><li><strong>@-mention 文件</strong> 而不是说出文件名，以节省读取调用</li><li><strong>Add quiet flags to noisy commands</strong> to avoid cluttered context</li><li><strong>为嘈杂的命令添加安静标志</strong> 以避免上下文混乱</li><li><strong>Run <code>/context</code></strong> in fresh sessions to see what’s loaded and cut unnecessary items</li><li><strong>在新会话中运行 <code>/context</code></strong> 查看加载了哪些内容并删除不必要的项目</li><li><strong>Run <code>/compact</code></strong> before long breaks while the cache is still active</li><li><strong>在长时间休息前运行 <code>/compact</code></strong> 同时保留缓存</li></ul><h2 id="maximizing-value-最大化价值"><a href="#maximizing-value-最大化价值" class="headerlink" title="Maximizing value 最大化价值"></a>Maximizing value 最大化价值</h2><p>Until pretty recently, the tools you wrote code with were a flat fee (or free). Your editor cost the same whether you fixed one test or fifty that afternoon, so an individual task didn’t really have a price of its own.</p><p>直到最近，你编写代码的工具都是固定费用（或免费的）。无论你是下午修了一个测试还是五十个测试，你的编辑器费用都是一样的，所以单个任务本身并没有真正的价格。</p><p>With agentic coding tools like Claude Code, it does. The same completed task can also cost different amounts depending on how you use it.</p><p>但对于像 Claude Code 这样的智能编程工具来说，情况并非如此。完成相同的任务也可能因使用方式不同而产生不同的费用。</p><p>In one session, Claude reads the test and the file it covers, makes the edit, and is done in a handful of turns. In another, it greps around the repo first, reads a dozen files on its way to the same two, and every one of those turns also drags along everything else that’s been read into the conversation since this morning.</p><p>在一个会话中，Claude 读取测试及其覆盖的文件，进行编辑，几个回合就完成了。在另一个会话中，它先在仓库中搜索，读取十几个文件才到达同样的两个文件，而且每个回合都携带着自早上以来读取到对话中的所有其他内容。</p><p>It’s the same fix, but you spent a different number of tokens on it, and the whole time the model was also having to think about ten files it didn’t need.</p><p>这是相同的修复，但你花费了不同数量的 token，而且整个过程中模型还需要思考它不需要的十个文件。</p><p>Being efficient with tokens doesn’t mean using fewer of them overall. It means making sure the ones you do use go towards the thing you actually asked for.</p><p>高效使用 token 并不意味着总体上使用更少。而是确保你使用的 token 都用于你实际请求的事情上。</p><p>So let’s look at what decides the price of a token, then what decides how many of them a session sends, and along the way, what that means for how you run a session.</p><p>所以让我们看看什么决定了 token 的价格，然后是什么决定了一个会话发送多少 token，以及这对你如何运行会话意味着什么。</p><h2 id="what-decides-the-price-of-a-token-什么决定了-token-的价格"><a href="#what-decides-the-price-of-a-token-什么决定了-token-的价格" class="headerlink" title="What decides the price of a token 什么决定了 token 的价格"></a>What decides the price of a token 什么决定了 token 的价格</h2><p>You’re billed per token, but what you’re actually paying for is inference: the time it takes a GPU (or a TPU, or whatever the model happens to be running on) to run the model over your tokens.</p><p>你是按 token 计费的，但你实际支付的是推理：GPU（或 TPU，或模型运行所在的任何硬件）在你的 token 上运行模型所需的时间。</p><p>Three things decide how much of that time a token takes: which model you’re running, whether it’s an input token (going in) or an output token (coming out), and whether it was cached.</p><p>三件事决定了每个 token 花费多少时间：你运行的模型、它是输入 token（进入）还是输出 token（出去）、以及它是否被缓存。</p><h3 id="model-模型"><a href="#model-模型" class="headerlink" title="Model 模型"></a>Model 模型</h3><p>A bigger model does more work on both input and output tokens. Which model is worth it for which kind of work is a topic on its own, and we covered it in a previous post.</p><p>更大的模型在输入和输出 token 上做更多的工作。哪种模型对哪种工作值得，这是一个独立的话题，我们在前面的文章中已经讨论过。</p><p>For this post, all you need to know is that everything else we’re about to cover gets multiplied by the model’s price: use a larger model when the problem is genuinely hard or ambiguous, and a smaller one when the work is routine.</p><p>对于这篇文章，你需要知道的是，我们接下来要讨论的所有内容都会乘以模型的价格：当问题确实困难或模糊时使用更大的模型，当工作是常规的时使用更小的模型。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/ea275de25fb2e249c444e0cd61390795.png" alt="Model size comparison showing Haiku, Sonnet, Opus, and Fable with Haiku at smallest" loading="lazy"></p><h3 id="input-and-output-tokens-输入和输出-token"><a href="#input-and-output-tokens-输入和输出-token" class="headerlink" title="Input and output tokens 输入和输出 token"></a>Input and output tokens 输入和输出 token</h3><p>A request goes through the GPU in two phases, and they cost different amounts.</p><p>一个请求经过 GPU 两个阶段，成本不同。</p><p>First, during prefill, the model reads your request and context: the system prompt, your conversation history, and the files you attached. Those are your input tokens.</p><p>首先，在预填充期间，模型读取你的请求和上下文：系统提示、你的对话历史以及你附加的文件。这些是你的输入 token。</p><p>Then, during decode, it writes output tokens: its thinking, the tool calls it makes, and the text you see. This happens one token at a time; a 200-token response is 200 runs of the model, one after the other. Per token, decode keeps the GPU busy for a lot longer, which is why output is priced at roughly 5x input.</p><p>然后，在解码期间，它写入输出 token：它的思考、它做出的工具调用以及你看到的文本。这是一个 token 一个 token 发生的；一个 200 token 的响应是模型的 200 次运行，一次接一次。每个 token，解码让 GPU 忙碌的时间长得多，这就是为什么输出价格大约是输入的 5 倍。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/446fd54fa93592f0aea90cd5cfda4d18.png" alt="Input vs output token pricing diagram" loading="lazy"></p><p>A lot of the output tokens in a session are thinking tokens, and how much thinking the model does per turn is what the effort level controls. Like the model, the level you pick with <code>/effort</code> gets multiplied by the output price.</p><p>会话中很多输出 token 是思考 token，模型每轮做多少思考是由努力级别控制的。和模型一样，你用 <code>/effort</code> 选择的级别会乘以输出价格。</p><h3 id="prompt-caching-提示缓存"><a href="#prompt-caching-提示缓存" class="headerlink" title="Prompt caching 提示缓存"></a>Prompt caching 提示缓存</h3><p>If a request starts with exactly the same tokens as a request the server just saw, the state for that shared beginning comes out the same, so the server can keep it around from last time and only prefill whatever comes after it. This is called prompt caching.</p><p>如果一个请求以与服务器刚刚看到的请求完全相同的 token 开头，共享开头的状态结果相同，所以服务器可以保留上次的内容，只预填充之后的部分。这叫做提示缓存。</p><p>Reading from the cache costs 0.1x the input price, because the server loads the state instead of computing it. Writing tokens into the cache costs a bit more than normal input, up to 2x, since the server also has to hold on to the state afterwards. But the write happens once per token, and the 0.1x reads happen on every turn after it.</p><p>从缓存读取的成本是输入价格的 0.1 倍，因为服务器加载状态而不是计算它。将 token 写入缓存的成本比正常输入略高，最高 2 倍，因为服务器之后还需要保留状态。但写入每个 token 只发生一次，而 0.1 倍的读取在之后的每个回合都会发生。</p><p>Claude Code manages the prompt cache on every request, there’s nothing to turn on. However you can break it, so it’s important to know how to avoid these cost spikes.</p><p>Claude Code 在每个请求上管理提示缓存，无需开启任何东西。但你可以破坏它，所以了解如何避免这些成本飙升很重要。</p><h3 id="an-example-一个例子"><a href="#an-example-一个例子" class="headerlink" title="An example 一个例子"></a>An example 一个例子</h3><p>Say we type “fix the failing test in src&#x2F;test&#x2F;auth.test.js” and send it to Claude.</p><p>假设我们输入”修复 src&#x2F;test&#x2F;auth.test.js 中的失败测试”并发送给 Claude。</p><p>Claude Code assembles the first request out of the system prompt (tool definitions included), your message, and the attached file. Nothing in the cache, so the whole thing goes out at full input price. The model responds with a Read call.</p><p>Claude Code 将系统提示（包含工具定义）、你的消息和附加的文件组装成第一个请求。缓存中没有内容，所以整个内容以全输入价格发出。模型以一个读取调用响应。</p><p>The model can’t fix a test it hasn’t seen, so it thinks for a moment and responds with a Read call for <code>src/auth/login.js</code>. Now the cache looks like:</p><p>模型无法修复它没有见过的测试，所以它思考了一会儿，然后用 <code>src/auth/login.js</code> 的读取调用响应。现在缓存看起来是：</p><ul><li>Request 1: system prompt + message + auth.test.js → full input price</li><li>请求 1：系统提示 + 消息 + auth.test.js → 全输入价格</li><li>Request 2: same prefix + Read result → cache read (0.1x)</li><li>请求 2：相同前缀 + 读取结果 → 缓存读取 (0.1x)</li></ul><p>Now the model wants the file under test (output). Another Read, another append, and everything goes out again: requests 1 and 2 from the cache, the second file at full price (input).</p><p>现在模型想要测试下的文件（输出）。又是一次读取，又是一次追加，所有内容再次发出：请求 1 和 2 从缓存中读取，第二个文件以全价格（输入）发出。</p><p>The model responds with an Edit (output). Claude Code applies it, appends the result, and sends everything again. Same story: the Edit and its result are new, everything in front of them is a cache read (input).</p><p>模型以一个编辑（输出）响应。Claude Code 应用它，追加结果，再次发送所有内容。同样：编辑及其结果是新的，前面的一切都是缓存读取（输入）。</p><p>The model runs the tests. Passes, in this case. Claude Code appends the test output, and out it goes again: the whole conversation so far at 0.1x.</p><p>模型运行测试。在这个例子中通过了。Claude Code 追加测试输出，再次全部发出：到目前为止的整个对话以 0.1x 发出。</p><p>The tests pass, and the model responds with a short summary (output). No tool call means nothing to append and no request 6, so we’re done.</p><p>测试通过了，模型以一个简短的总结响应（输出）。没有工具调用意味着没有东西可追加，也没有请求 6，所以我们完成了。</p><p>That’s five requests for one small fix, and every one of them contained the entire conversation up to that point. A typical turn is lopsided: tens of thousands of tokens going in, a few hundred coming out. But only what’s new in that turn gets prefilled at full price.</p><p>这是一个小修复的五个请求，每个都包含到那时为止的整个对话。一个典型的回合是倾斜的：数万个 token 进入，几百个出来。但只有那一轮中的新内容以全价格预填充。</p><p>That’s the whole per-turn bill: cache reads on the history, full input price on whatever’s new, and the output price on the response.</p><p>这就是每个回合的全部账单：历史的缓存读取、新内容的全输入价格，以及响应的输出价格。</p><h3 id="what-breaks-the-cache-什么会破坏缓存"><a href="#what-breaks-the-cache-什么会破坏缓存" class="headerlink" title="What breaks the cache 什么会破坏缓存"></a>What breaks the cache 什么会破坏缓存</h3><p>The cache has to match from the very start of the request forward, and requests always go out in the same order: tool definitions, then the system prompt, then the conversation (with the newest addition at the end).</p><p>缓存必须从请求的最开始向前匹配，请求总是以相同的顺序发出：工具定义，然后是系统提示，然后是对话（最新的添加在最后）。</p><p>If anything in that prefix changes, everything behind it gets prefilled again. A tool result appended to the end of the conversation is the ideal case, since nothing is behind it. What throws the cache away is anything that changes the request further towards the front, or changes what the cache is keyed on:</p><p>如果前缀中有任何变化，后面的一切都会重新预填充。追加到对话末尾的工具结果是理想情况，因为后面什么都没有。破坏缓存的是任何使请求更靠前部分发生变化的东西，或者改变缓存键的东西：</p><ul><li><strong><code>/model</code></strong>: every model has its own cache</li><li><strong><code>/model</code></strong>：每个模型有自己独立的缓存</li><li><strong><code>/effort</code></strong>: the effort level is part of what the cache is keyed on</li><li><strong><code>/effort</code></strong>：努力级别是缓存键的一部分</li><li><strong>Fast mode</strong>: also part of the key</li><li><strong>快速模式</strong>：也是键的一部分</li><li><strong><code>/compact</code></strong>: the conversation gets replaced with a shorter one</li><li><strong><code>/compact</code></strong>：对话被替换为一个更短的</li><li><strong>Time</strong>: the cache expires after an hour on a subscription or five minutes on an API key</li><li><strong>时间</strong>：缓存在订阅者一小时后或 API 密钥五分钟后过期</li></ul><p>None of this means you should never switch models or effort. It means there are cheap moments to do it, the start of a session or right after a <code>/compact</code>.</p><p>这并不意味着你永远不应该切换模型或努力级别。这意味着有一些便宜的时刻可以这样做，比如会话开始时或 <code>/compact</code> 之后。</p><h2 id="what-decides-how-many-tokens-a-session-sends-什么决定一个会话发送多少-token"><a href="#what-decides-how-many-tokens-a-session-sends-什么决定一个会话发送多少-token" class="headerlink" title="What decides how many tokens a session sends 什么决定一个会话发送多少 token"></a>What decides how many tokens a session sends 什么决定一个会话发送多少 token</h2><p>The main thing to know here is that nothing gets sent just once. Everything that ends up in the conversation, a file Claude read or the output of a command it ran, gets sent again on every turn after it, for the rest of the session.</p><p>这里需要知道的主要是一点：没有任何东西只发送一次。每个进入对话的内容——Claude 读取的文件或它运行的命令输出——在之后的每个回合都会再次发送，直到会话结束。</p><p>It’s cached, so each of those re-sends is cheap, but cheap isn’t nothing, and it’s taking up room in the context the model has to think around on every turn too.</p><p>它被缓存了，所以每次重新发送都便宜，但便宜不等于免费，而且它也在占用模型每个回合都必须思考的上下文空间。</p><p>That’s really the whole cost model of a session: how many tokens end up in the context, how many turns they stay there, and how many contexts you’re running at the same time.</p><p>这才是会话的完整成本模型：多少 token 进入上下文，它们在那里停留多少回合，以及你同时运行多少个上下文。</p><h3 id="what-ends-up-in-the-context-什么进入上下文"><a href="#what-ends-up-in-the-context-什么进入上下文" class="headerlink" title="What ends up in the context 什么进入上下文"></a>What ends up in the context 什么进入上下文</h3><p>Part of what’s in the context is there before you type anything: the tool definitions, the system prompt, <code>CLAUDE.md</code>, and whatever else gets loaded at startup.</p><p>上下文中有些内容在你输入任何东西之前就存在：工具定义、系统提示、<code>CLAUDE.md</code> 以及启动时加载的任何其他内容。</p><p>Nearly everything else that gets added during the session is tool results: the files Claude reads, and the output of the commands it runs.</p><p>会话期间添加的几乎所有其他内容都是工具结果：Claude 读取的文件以及它运行的命令输出。</p><p>How much Claude reads mostly comes down to how much it has to figure out on its own. If you say “the tests are failing”, it first has to find out which tests: a grep or two, a few files opened to see which one is relevant, and all of those results stay in the context long after they’ve stopped being useful.</p><p>Claude 读取多少主要取决于它需要自己弄清楚多少。如果你说”测试失败了”，它首先需要找出是哪些测试：一两个 grep，几个打开的文件来查看哪个相关，所有这些结果都会在上下文中有很长的保留时间，即使它们已经不再有用。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/eebba2812669a8cdea68dc4183d2a9be.png" alt="Context accumulation in sessions" loading="lazy"></p><p>“Fix the failing test in src&#x2F;test&#x2F;auth.test.js” didn’t require any reading to understand. “The tests are failing” required some, and “something is broken” requires even more.</p><p>“修复 src&#x2F;test&#x2F;auth.test.js 中的失败测试”不需要任何读取就能理解。”测试失败了”需要一些读取，而”什么东西坏了”需要更多。</p><p>The other thing that fills up the context is the output of the commands Claude runs. Every time it runs your tests, a build, or a git log, whatever that prints gets appended to the conversation just like a file it read, and stays there for the same number of turns.</p><p>另一个填满上下文的东西是 Claude 运行命令的输出。每次它运行你的测试、构建或 git log，无论打印什么都会被追加到对话中，就像它读取的文件一样，并且在相同数量的回合中保留。</p><p>Really big outputs are actually fine: after 30,000 characters Claude Code writes the output to a file and only puts a short preview and the path in the conversation (and you can configure the threshold with <code>/output-lines</code>).</p><p>非常大的输出实际上没问题：超过 30,000 个字符后，Claude Code 会将输出写入文件，只在对话中放置一个简短的预览和路径（你也可以用 <code>/output-lines</code> 配置这个阈值）。</p><p>The problem is everything under that. A test runner that prints 400 passing tests one line at a time comes in under the limit, and those 400 lines are now part of every remaining turn.</p><p>问题是在这个阈值以下的任何东西。一个一次打印 400 个通过测试的测试运行器低于这个限制，但这 400 行现在成为了每个剩余回合的一部分。</p><p>Claude will often take care of this for you with flags and tail, and if you’d rather not leave it up to Claude, there’s a small hook in the docs that rewrites noisy commands before they run so only the lines that matter come back.</p><p>Claude 通常会用 flags 和 tail 为你处理这个问题，如果你不想交给 Claude，文档中有一个小 hook 可以重写嘈杂的命令，只返回重要的行。</p><h3 id="how-many-turns-it-stays-there-它在那里停留多少回合"><a href="#how-many-turns-it-stays-there-它在那里停留多少回合" class="headerlink" title="How many turns it stays there 它在那里停留多少回合"></a>How many turns it stays there 它在那里停留多少回合</h3><p>One long session costs more than the same work spread over a few short ones, and by more than you’d think, because turn 40 is also re-reading the 39 turns before it. You want the context in your session to be short and relevant, so don’t carry one task’s context into the next: <code>/clear</code> when you start something new, and <code>/compact</code> when the earlier part of the same task is done.</p><p>一个长会话比将相同的工作分散到几个短会话中花费更多，而且比你想象的更多，因为第 40 回合也在重读之前的 39 个回合。你希望会话中的上下文简短且相关，所以不要将一个任务的上下文带入下一个：在开始新任务时用 <code>/clear</code>，在同一任务的前面部分完成后用 <code>/compact</code>。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/84f9d7b8bb20c13a8e296036d9fe5b32.png" alt="Context management illustration" loading="lazy"></p><p>Keep an eye on turns that happen when you’re not typing, too. A <code>/review</code> command might run for a few minutes and read a dozen files, and unless you run <code>/compact</code> before you pick up the next task, those files stay in your context.</p><p>也要注意在你没有输入时发生的回合。<code>/review</code> 命令可能运行几分钟并读取十几个文件，除非你在开始下一个任务之前运行 <code>/compact</code>，否则这些文件会留在你的上下文中。</p><h3 id="subagents-子代理"><a href="#subagents-子代理" class="headerlink" title="Subagents 子代理"></a>Subagents 子代理</h3><p>The other way to keep something out of your context is to have it happen in a different one, which is what subagents are for. A subagent gets its own context window, with its own system prompt, the tools, and your <code>CLAUDE.md</code>, but not your conversation. It runs its own turns, and the only thing that comes back to the main session is its answer.</p><p>另一个让东西远离你上下文的方法是让它在不同的上下文中发生，这就是子代理的用途。子代理获得自己的上下文窗口，有自己的系统提示、工具和你的 <code>CLAUDE.md</code>，但没有你的对话。它运行自己的回合，唯一回到主会话的是它的答案。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/4c0f3ba59301e2a79863f75c5fcdd45d.png" alt="Subagent diagram showing main session with separate subagent context" loading="lazy"></p><p>The downside of not having your conversation is that a subagent sometimes has to re-read things the main session already had, and it’s paying for its own turns while it does. For a small job it’s just overhead.</p><p>没有你的对话的缺点是子代理有时需要重新读取主会话已经有的东西，而且它在这样做时支付自己的回合费用。对于小任务来说这只是开销。</p><p>It pays off when a job produces a lot of output you don’t need to keep, like going through a log. Claude will often reach for one on its own for that kind of thing, and you can ask for one directly when it doesn’t (“go through this log in a subagent”). Just keep in mind that the main session only gets back what the subagent chose to report.</p><p>当一项工作产生大量你不需要保留的输出时，比如遍历日志，这就值得了。Claude 通常会自己为此使用一个，你也可以在它没有主动使用时直接要求它（”在一个子代理中遍历这个日志”）。只要记住主会话只能取回子代理选择报告的内容。</p><h2 id="where-to-look-first-首先关注什么"><a href="#where-to-look-first-首先关注什么" class="headerlink" title="Where to look first 首先关注什么"></a>Where to look first 首先关注什么</h2><p>Of everything above, four things are worth keeping an eye on, roughly in order of how much they cost:</p><p>在上述所有内容中，有四件事值得关注，大致按成本排序：</p><ol><li>Model selection 模型选择</li><li>Effort level 努力级别</li><li>Context size 上下文大小</li><li>Session length 会话长度</li></ol><h2 id=""><a href="#" class="headerlink" title=""></a><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/2c7a5affbabff4aa40bd88356b3db711.png" alt="Four key areas to monitor: Model, Effort, Context, Sessions" loading="lazy"></h2><h2 id="additional-tips-额外技巧"><a href="#additional-tips-额外技巧" class="headerlink" title="Additional Tips 额外技巧"></a>Additional Tips 额外技巧</h2><h3 id="file-referencing-文件引用"><a href="#file-referencing-文件引用" class="headerlink" title="File Referencing 文件引用"></a>File Referencing 文件引用</h3><p>@-mention files instead of naming them. The file gets attached to your message directly, which saves a Read call.</p><p>@-mention 文件而不是说出文件名。文件直接附加到你的消息上，这节省了一次读取调用。</p><p><img src="/posts/maximizing-the-value-of-your-claude-code-sessions/images/9e7e06c390c4473bf2674a28b1e74b7c.png" alt="Two scenarios comparing file referencing methods" loading="lazy"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>[翻译]Go 1.27 Release Notes</title>
      <link>https://yangshifu.xyz/posts/go-127-release-notes/</link>
      <description>Golang 1.27 发布注记翻译</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/golang/">golang</category>
      <pubDate>Fri, 21 Aug 2026 20:42:56 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://go.dev/doc/go1.27">Go 1.27 Release Notes</a></li><li>原作者：The Go Team</li><li>原文发表于：2026-08-19</li></ul></blockquote><h2 id="go-1-27-简介"><a href="#go-1-27-简介" class="headerlink" title="Go 1.27 简介"></a>Go 1.27 简介</h2><p>最新的 Go 发行版 1.27 于 <a target="_blank" rel="noopener" href="https://go.dev/doc/devel/release#go1.27.0">2026 年 8 月</a> 发布，距 <a target="_blank" rel="noopener" href="https://go.dev/doc/go1.26">Go 1.26</a> 六个月。本次更新主要集中在工具链、运行时和标准库的实现上。一如既往，本版本继续遵守 Go 1 的<a target="_blank" rel="noopener" href="https://go.dev/doc/go1compat">兼容性承诺</a>。我们预期几乎所有现有的 Go 程序都能继续编译并运行。</p><span id="more"></span><h2 id="语言变更"><a href="#语言变更" class="headerlink" title="语言变更"></a>语言变更</h2><p>Go 1.27 现已支持<a target="_blank" rel="noopener" href="https://go.dev/issue/77273">泛型方法</a>：<a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Method_declarations">方法声明</a>可以声明自己的<a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Type_parameter_declarations">类型参数</a>。这一广受期待的变化使得泛型函数可以放在某个具体数据类型的命名空间中，而不再必须把这类函数声明在整个包的作用域里。例如，<code>math/rand/v2</code> 现在也声明了签名为 <code>(*Rand) N[Int intType](Int) Int</code> 的泛型方法；此前它只有一个泛型函数 <code>N[Int intType](Int) Int</code>（详见下文 <code>math/rand/v2</code> 一节）。需要注意的是，<a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Interface_types">接口</a>方法不能声明类型参数，接口方法也不能由泛型方法来实现。</p><p><a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Composite_literals">结构体字面量</a>中的键现在可以是该结构体类型的任意合法<a target="_blank" rel="noopener" href="https://go.dev/issue/9859">字段选择器</a>，而不再仅限于结构体的（顶层）字段名。</p><blockquote><p>译者注：这意味着嵌入结构体中的字段可以直接作为字面量的键。官方博客中的示例如下：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Habitat <span class="hljs-keyword">struct</span> &#123;<br>    Burrow <span class="hljs-type">string</span><br>&#125;<br><br><span class="hljs-keyword">type</span> Gopher <span class="hljs-keyword">struct</span> &#123;<br>    Name    <span class="hljs-type">string</span><br>    Habitat <span class="hljs-comment">// 嵌入结构体</span><br>&#125;<br><br><span class="hljs-comment">// Go 1.27 允许直接使用 Burrow 作为键</span><br>g := Gopher&#123;<br>    Name:   <span class="hljs-string">&quot;Gopher&quot;</span>,<br>    Burrow: <span class="hljs-string">&quot;Burrow #42&quot;</span>,<br>&#125;<br></code></pre></td></tr></table></figure></blockquote><p>函数类型推断已<a target="_blank" rel="noopener" href="https://go.dev/issue/77245">推广</a>：只要把泛型函数<a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Assignability">赋值</a>给（或转换到）匹配的函数类型，都可以进行推断。</p><blockquote><p>译者注：官方博客中的示例如下。泛型函数现在可以在复合字面量、类型转换和 channel 发送中省略显式类型实参：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GenericFormatter</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(v T)</span></span> <span class="hljs-type">string</span> &#123;<br>    <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;value: %v&quot;</span>, v)<br>&#125;<br><br><span class="hljs-keyword">type</span> IntFormatter <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(<span class="hljs-type">int</span>)</span></span> <span class="hljs-type">string</span><br><br>formatters := []IntFormatter&#123;GenericFormatter&#125;<br>fn := IntFormatter(GenericFormatter)<br>ch := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> IntFormatter, <span class="hljs-number">1</span>)<br>ch &lt;- GenericFormatter<br></code></pre></td></tr></table></figure></blockquote><h2 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h2><p><code>compile</code>、<code>link</code>、<code>asm</code>、<code>cgo</code>、<code>cover</code> 和 <code>pack</code> 工具现在支持响应文件（<code>@file</code>）解析。响应文件中的参数以空白分隔，支持单引号字符串、双引号字符串、转义序列，以及反斜杠加换行的续行。该格式与 GCC 的响应文件实现兼容，以便与现有构建系统互操作。</p><h3 id="go-命令"><a href="#go-命令" class="headerlink" title="Go 命令"></a>Go 命令</h3><p><code>go</code> 命令不再支持 <code>bzr</code> 版本控制系统，也无法再直接获取托管在 <code>bzr</code> 服务器上的模块。</p><h4 id="godebug"><a href="#godebug" class="headerlink" title="GODEBUG"></a><code>GODEBUG</code></h4><p>从 Go 1.27 开始，如果某个已被移除支持的 <code>GODEBUG</code> 设置（例如 <code>asynctimerchan</code>，见<a href="#%E8%BF%90%E8%A1%8C%E6%97%B6">下文</a>）出现在 <code>go.mod</code> 文件（<code>godebug</code> 条目）或 <code>.go</code> 源文件（<code>//go:debug</code> 注释）中，<code>go</code> 命令现在能够识别它们。若这些设置被设为移除前确立的最终默认值，则会被接受；若被设为旧值，<code>go</code> 命令将失败。这一变更符合 <a target="_blank" rel="noopener" href="https://go.dev/doc/go1compat">Go 1 兼容性保证</a> 的精神：即使相应设置的支持已被移除，现有程序只要设置的是仍被接受的 <code>GODEBUG</code> 值，就可以继续构建和运行，无需修改。</p><h4 id="go-test"><a href="#go-test" class="headerlink" title="go test"></a><code>go test</code></h4><p><code>go test</code> 现在默认会运行 <code>stdversion</code> vet 检查。它会报告引用文件中使用了对于当前生效的 Go 版本来说过新的标准库符号；该版本由 <code>go.mod</code> 中的 <code>go</code> 指令以及文件上的 build tag 决定。</p><p><code>go test -json</code> 现在会为 <code>&quot;Action&quot;:&quot;output&quot;</code> 行标注一个可选的新字段 <code>&quot;OutputType&quot;</code>，用于说明输出类型。目前可能的取值包括 “error”、“error-continue” 和 “frame”。详见 <a target="_blank" rel="noopener" href="https://pkg.go.dev/cmd/test2json#hdr-Output_Format">cmd&#x2F;test2json help</a>。</p><h4 id="go-doc"><a href="#go-doc" class="headerlink" title="go doc"></a><code>go doc</code></h4><p><code>go doc</code> 命令现在支持 <code>package@version</code> 语法，例如 <code>go doc example.com/pkg@v1.2.3</code>。</p><p><code>go doc</code> 命令现在接受 <code>-ex</code> 命令行选项，用于列出给定包或符号的可执行示例。当命令行传入示例名称（例如 <code>go doc bytes.ExampleBuffer</code>）时，<code>go doc</code> 现在会连同注释一起打印示例源码。</p><h4 id="go-fix"><a href="#go-fix" class="headerlink" title="go fix"></a><code>go fix</code></h4><p><code>go fix</code> 命令新增了若干 modernizer（<code>atomictypes</code>、<code>embedlit</code>、<code>slicesbackward</code> 和 <code>unsafefuncs</code>）。</p><p>出于风格方面的考虑，现有的 <code>fmtappendf</code> 分析器已被移除。</p><p>现有的 <code>waitgroup</code> 分析器已重命名为 <code>waitgroupgo</code>，以避免歧义。</p><h4 id="go-mod-tidy"><a href="#go-mod-tidy" class="headerlink" title="go mod tidy"></a><code>go mod tidy</code></h4><p>对于在 <code>go.mod</code> 中指定 <code>go 1.27</code> 或更高版本的模块，<code>go mod tidy</code> 现在会自动合并重复的 require 块。这能确保文件保持干净、标准的结构：最多两个 require 块，一个用于直接依赖，一个用于间接依赖。</p><p>合并过程中会保留依附于依赖项的现有注释块。如果某个注释块同时关联了直接依赖和间接依赖，该注释块会被合并并挂到新的直接依赖块上。</p><p>此前，如果 <code>go.mod</code> 积累了多个互不相连的 require 块（通常来自手工编辑、未解决的 Git 合并冲突或遗留升级），<code>go mod tidy</code> 会原样保留这些多余块，甚至无意中再创建新块。现在该工具会严格强制两块布局，将分散的依赖归并到各自对应的块中，并自动清理模块文件结构。</p><h3 id="trace"><a href="#trace" class="headerlink" title="Trace"></a>Trace</h3><p>当 <code>go tool trace</code> 的 <code>-http</code> 命令行选项只传入端口（例如 <code>-http=:6060</code>）时，现在会把监听地址限制为 localhost。这一变更使 <code>go tool trace</code> 与 <code>go tool pprof</code> 的 <code>-http</code> 标志行为保持一致。若要监听所有地址，需要显式指定地址（例如 <code>-http=0.0.0.0:6060</code>）。</p><h2 id="运行时"><a href="#运行时" class="headerlink" title="运行时"></a>运行时</h2><p>对于 <code>go</code> 指令配置为 Go 1.27 或更高版本的模块，traceback 现在会在标题行中包含 <a target="_blank" rel="noopener" href="https://pkg.go.dev/runtime/pprof"><code>runtime/pprof</code></a> 的 goroutine label。可通过 <code>tracebacklabels=0</code> 这一 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-126"><code>GODEBUG</code></a> 设置（添加于 Go 1.26）关闭该行为。该退出开关预计会长期保留，以防 goroutine label 日后包含不应出现在 traceback 中的敏感信息。</p><p><code>asynctimerchan</code> <code>GODEBUG</code> 设置（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-123">Go 1.23</a>）已被永久移除。<a target="_blank" rel="noopener" href="https://pkg.go.dev/time">time</a> 包创建的 channel 现在始终是无缓冲（同步）的，与 <code>GODEBUG</code> 设置无关。</p><h3 id="更快的内存分配"><a href="#更快的内存分配" class="headerlink" title="更快的内存分配"></a>更快的内存分配</h3><p>编译器现在会生成调用按尺寸特化的内存分配例程，使部分小型（小于 80 字节）内存分配的开销最多降低 30%。具体收益因负载而异，但在真实的高分配程序中，整体提升预计约为 1%。这会使二进制体积增加约 60 KB（与负载无关）。如果发现任何回归，请<a target="_blank" rel="noopener" href="https://go.dev/issue/new">提交 issue</a>。可以在构建时设置 <code>GOEXPERIMENT=nosizespecializedmalloc</code> 来关闭该优化。该退出设置预计会在 Go 1.28 中移除。</p><h3 id="goroutine-泄漏-profile"><a href="#goroutine-泄漏-profile" class="headerlink" title="Goroutine 泄漏 profile"></a>Goroutine 泄漏 profile</h3><p>一种报告泄漏 goroutine 的新 profile 类型此前在 <a target="_blank" rel="noopener" href="https://go.dev/doc/go1.26#goroutineleak-profiles">Go 1.26</a> 中作为实验提供，现在已正式可用。该 profile 类型名为 <code>goroutineleak</code>，由 <a target="_blank" rel="noopener" href="https://pkg.go.dev/runtime/pprof"><code>runtime/pprof</code></a> 包支持，也可通过 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http/pprof"><code>net/http/pprof</code></a> 端点 <code>/debug/pprof/goroutineleak</code> 获取。</p><p>所谓<em>泄漏</em>的 goroutine，是指阻塞在某个并发原语（channel、<a target="_blank" rel="noopener" href="https://pkg.go.dev/sync#Mutex"><code>sync.Mutex</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/sync#Cond"><code>sync.Cond</code></a> 等）上、且该原语不可能再被解除阻塞的 goroutine。运行时借助垃圾回收器检测泄漏：如果 goroutine G 阻塞在并发原语 P 上，而 P 从任何可运行的 goroutine、以及这些 goroutine 可能解除阻塞的其他 goroutine 都不可达，那么 P 无法被解除阻塞，G 也就永远无法被唤醒。虽然无法在所有情况下都检测出永久阻塞的 goroutine，但这种方法能覆盖很大一类泄漏。</p><p>由于该技术建立在可达性之上，如果泄漏是由阻塞在全局变量、或可运行 goroutine 局部变量所引用的并发原语上引起的，运行时可能无法识别。</p><p>示例见 <a target="_blank" rel="noopener" href="https://go.dev/doc/go1.26#goroutineleak-profiles">Go 1.26 发行说明</a>。</p><p>特别感谢 Uber 的 Vlad Saioc 贡献了这项工作。</p><p><code>goroutineleakprofile</code> <code>GOEXPERIMENT</code> 设置现已删除。</p><h2 id="编译器"><a href="#编译器" class="headerlink" title="编译器"></a>编译器</h2><p>编译器现在会把 <code>//line</code> 或 <code>/*line*/</code> 指令中的相对文件名解析为相对于包含该指令的文件所在目录，这与 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/scanner"><code>go/scanner</code></a> 的行为一致。绝对文件名不受影响。参见 <a target="_blank" rel="noopener" href="https://go.dev/issue/70478">#70478</a>。</p><p>编译器现在会为函数字面量（闭包）生成更简单的名字。此前，当包含该字面量的函数被内联时，函数字面量的名字可能变得很长。现在无论是否内联，编译器都会为该函数字面量选择相同的名字。它还可能把同一函数字面量的多个实例（因其包含函数被内联而产生）合并，在编译后的二进制中共享同一段代码。这一变更不影响 Go 代码的功能。检查符号名的测试可能需要更新，不过建议不要依赖函数字面量的名字。对于<a target="_blank" rel="noopener" href="https://pkg.go.dev/reflect#Value.Pointer">错误地</a>比较函数代码指针是否相等的程序，该问题在 Go 1.27 中可能更容易暴露：捕获了不同闭包数据的函数字面量，在更多情况下可能具有相同的代码指针。</p><h2 id="链接器"><a href="#链接器" class="headerlink" title="链接器"></a>链接器</h2><p>在面向 macOS 时，链接器现在接受 <code>-macos</code> 和 <code>-macsdk</code> 命令行选项，用于在 <code>LC_BUILD_VERSION</code> load command 中指定 OS 和 SDK 版本。默认情况下，它会选择当前仍受支持的最老 macOS 版本（目前为 <a href="#darwin">13.0.0</a>）以及较新的 SDK 版本（目前为 26.2.0）。</p><h2 id="标准库"><a href="#标准库" class="headerlink" title="标准库"></a>标准库</h2><h3 id="新增-encoding-json-v2-与-encoding-json-jsontext-包"><a href="#新增-encoding-json-v2-与-encoding-json-jsontext-包" class="headerlink" title="新增 encoding&#x2F;json&#x2F;v2 与 encoding&#x2F;json&#x2F;jsontext 包"></a>新增 encoding&#x2F;json&#x2F;v2 与 encoding&#x2F;json&#x2F;jsontext 包</h3><p>现在提供了两个新包：</p><ul><li><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2"><code>encoding/json/v2</code></a> 是 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json"><code>encoding/json</code></a> 的一次重大修订。它提供 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#Marshal"><code>Marshal</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#MarshalWrite"><code>MarshalWrite</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#MarshalEncode"><code>MarshalEncode</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#Unmarshal"><code>Unmarshal</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#UnmarshalRead"><code>UnmarshalRead</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#UnmarshalDecode"><code>UnmarshalDecode</code></a>，这些函数都接受可变数量的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/v2#Options"><code>Options</code></a> 参数，用于配置编解码行为。</p></li><li><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/jsontext"><code>encoding/json/jsontext</code></a> 提供更底层的 JSON 语法处理。<a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/jsontext#Encoder"><code>Encoder</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/jsontext#Decoder"><code>Decoder</code></a> 将 JSON 作为 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/jsontext#Token"><code>Token</code></a> 与 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json/jsontext#Value"><code>Value</code></a> 的序列来操作，并维护状态机，以确保产出或消费的序列是合法的 JSON 文本。</p></li></ul><p>v2 包采用比 v1 更严格、也更利于互操作的默认行为：它会拒绝 JSON 字符串中的非法 UTF-8，并拒绝 JSON 对象中的重复名称。完整的行为差异以及可用于调整这些行为的选项，见 v1 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json#hdr-Migrating_to_v2"><code>encoding/json</code></a> 包文档。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json"><code>encoding/json</code></a> 包现在由 v2 实现驱动。编解码行为得以保留，但错误信息的具体文本可能不同。该包还新增了一批 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/json#Options"><code>Options</code></a>，可以把 v2 配置成按 v1 语义工作，从而不必一次性迁移到新 API。v1 API 将继续受支持，用户不必迁移。</p><p>Marshal 性能与此前实现大体持平，Unmarshal 性能则显著更快。</p><p>如果遇到与新实现的兼容性问题，可以在构建时设置 <code>GOEXPERIMENT=nojsonv2</code> 来关闭它，从而恢复原来的 v1 实现。该退出开关预计会在未来版本中移除。</p><p>背景和更多细节见<a target="_blank" rel="noopener" href="https://go.dev/issue/71497">提案 issue</a>。如果需要关闭新实现，请<a target="_blank" rel="noopener" href="https://go.dev/issue/new">提交 issue</a>。</p><h3 id="新增-crypto-mldsa-包"><a href="#新增-crypto-mldsa-包" class="headerlink" title="新增 crypto&#x2F;mldsa 包"></a>新增 crypto&#x2F;mldsa 包</h3><p>新的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/mldsa"><code>crypto/mldsa</code></a> 包实现了 FIPS 204 中规定的后量子 ML-DSA 签名方案。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509"><code>crypto/x509</code></a> 现在支持 ML-DSA 私钥、公钥和签名。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls"><code>crypto/tls</code></a> 现在在 TLS 1.3 中支持 ML-DSA 签名，并新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#MLDSA44"><code>MLDSA44</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#MLDSA65"><code>MLDSA65</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#MLDSA87"><code>MLDSA87</code></a> 这几个 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#SignatureScheme"><code>SignatureScheme</code></a> 值。</p><h3 id="新增-uuid-包"><a href="#新增-uuid-包" class="headerlink" title="新增 uuid 包"></a>新增 uuid 包</h3><p>新的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/uuid"><code>uuid</code></a> 包用于生成和解析 UUID。</p><h3 id="新增实验性-simd-包"><a href="#新增实验性-simd-包" class="headerlink" title="新增实验性 simd 包"></a>新增实验性 simd 包</h3><p>Go 1.27 引入了新的实验性 <a target="_blank" rel="noopener" href="https://pkg.go.dev/simd"><code>simd</code></a> 包，提供可移植、且与向量宽度无关的 SIMD 支持。在硬件指令可用时会使用硬件指令。该包通过在构建时设置环境变量 <code>GOEXPERIMENT=simd</code> 启用。</p><p><code>simd</code> 包在所有架构上都可用，并提供尺寸未指定的向量类型，例如 <a target="_blank" rel="noopener" href="https://pkg.go.dev/simd#Int8s"><code>Int8s</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/simd#Float32s"><code>Float32s</code></a>。它支持 <a target="_blank" rel="noopener" href="https://pkg.go.dev/simd/archsimd"><code>simd/archsimd</code></a> 包中那些跨架构、跨向量宽度有硬件支持或易于模拟的“可伸缩”操作子集。</p><p>更多细节见<a target="_blank" rel="noopener" href="https://go.dev/issue/78902">提案 issue</a>。</p><h3 id="实验性-simd-archsimd-包"><a href="#实验性-simd-archsimd-包" class="headerlink" title="实验性 simd&#x2F;archsimd 包"></a>实验性 simd&#x2F;archsimd 包</h3><p>Go 1.27 继续为始于 <a target="_blank" rel="noopener" href="https://go.dev/doc/go1.26#simd">Go 1.26</a> 的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/simd/archsimd/"><code>simd/archsimd</code></a> 包提供实验性 SIMD 操作支持。本版本修订了 amd64 API，并新增了对 arm64 “Neon” 128 位 SIMD 以及 WebAssembly 128 位 SIMD 的支持。<code>simd/archsimd</code> 包同样通过构建时设置环境变量 <code>GOEXPERIMENT=simd</code> 启用。</p><p>该包提供架构相关的 SIMD 操作。它在 wasm、arm64 和 amd64 上支持 128 位向量类型，并在部分 amd64 处理器上支持 256 位和 512 位向量类型。API 尚未视为稳定。</p><p>更多细节见<a target="_blank" rel="noopener" href="https://pkg.go.dev/simd/archsimd">包文档</a>和<a target="_blank" rel="noopener" href="https://go.dev/issue/73787">提案 issue</a>。</p><p>我们计划在未来版本中为更多架构提供支持，但该 API 有意做成架构相关，因此不可移植。</p><h3 id="标准库的其余变更"><a href="#标准库的其余变更" class="headerlink" title="标准库的其余变更"></a>标准库的其余变更</h3><h4 id="bytes"><a href="#bytes" class="headerlink" title="bytes"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/bytes/"><code>bytes</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/bytes#CutLast"><code>CutLast</code></a> 函数，按分隔符最后一次出现的位置切分 <code>[]byte</code>。它可以替代并简化一些常见的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/bytes#LastIndex"><code>LastIndex</code></a> 用法。</p><h4 id="compress-flate"><a href="#compress-flate" class="headerlink" title="compress/flate"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/compress/flate/"><code>compress/flate</code></a></h4><p>Go 1.27 提升了压缩速度。由于编码器实现变更，<a target="_blank" rel="noopener" href="https://pkg.go.dev/compress/flate#Writer"><code>Writer</code></a> 产出的精确编码结果可能与 Go 1.26 不同。因为 DEFLATE 是 <a target="_blank" rel="noopener" href="https://pkg.go.dev/archive/zip"><code>archive/zip</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/compress/gzip"><code>compress/gzip</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/compress/zlib"><code>compress/zlib</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/image/png"><code>image/png</code></a> 所使用的底层压缩，这些包的输出也可能发生变化。</p><h4 id="crypto"><a href="#crypto" class="headerlink" title="crypto"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/"><code>crypto</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto#MLDSAMu"><code>MLDSAMu</code></a> 这一 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto#Hash"><code>Hash</code></a> 值，用作 External μ ML-DSA 签名的信号机制。</p><h4 id="crypto-ecdsa"><a href="#crypto-ecdsa" class="headerlink" title="crypto/ecdsa"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/ecdsa/"><code>crypto/ecdsa</code></a></h4><p>如果提供了非 nil 的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto#SignerOpts"><code>SignerOpts</code></a>，<a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/ecdsa#PrivateKey.Sign"><code>PrivateKey.Sign</code></a> 现在会检查 hash 长度是否正确。</p><h4 id="crypto-tls"><a href="#crypto-tls" class="headerlink" title="crypto/tls"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls/"><code>crypto/tls</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#QUICConfig.ClientHelloInfoConn"><code>QUICConfig.ClientHelloInfoConn</code></a> 字段，用于指定 QUIC 服务端握手期间 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#ClientHelloInfo.Conn"><code>ClientHelloInfo.Conn</code></a> 所使用的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net#Conn"><code>net.Conn</code></a>。</p><p>现在支持 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#MLKEM1024"><code>MLKEM1024</code></a> 密钥交换。可以通过将其加入 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#Config.CurvePreferences"><code>Config.CurvePreferences</code></a> 来启用。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#Config.Rand"><code>Config.Rand</code></a> 现已弃用。若需要确定性测试，请使用 <a target="_blank" rel="noopener" href="https://pkg.go.dev/testing/cryptotest#SetGlobalRandom"><code>testing/cryptotest.SetGlobalRandom</code></a>。</p><p>即使使用了 <code>tlsmlkem=0</code> 或 <code>tlssecpmlkem=0</code> 这些 <code>GODEBUG</code> 选项，现在也可以在 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#Config.CurvePreferences"><code>Config.CurvePreferences</code></a> 中显式启用后量子混合密钥交换。这些选项原本就只应作用于 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#Config.CurvePreferences"><code>Config.CurvePreferences</code></a> 为 nil 时的默认集合。</p><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/tls#ConnectionState.LocalCertificate"><code>ConnectionState.LocalCertificate</code></a> 字段，包含握手期间呈现给对端的证书链。</p><p>以下 <code>GODEBUG</code> 设置已被永久移除：<code>tlsunsafeekm</code>（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-122">Go 1.22</a>）、<code>tlsrsakex</code>（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-122">Go 1.22</a>）、<code>tls3des</code>（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-123">Go 1.23</a>）、<code>tls10server</code>（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-122">Go 1.22</a>）以及 <code>x509keypairleaf</code>（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-123">Go 1.23</a>）。</p><h4 id="crypto-x509"><a href="#crypto-x509" class="headerlink" title="crypto/x509"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/"><code>crypto/x509</code></a></h4><p>解析到 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/pkix#Name"><code>pkix.Name</code></a> 字段时，现在支持更广泛的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/pkix#AttributeTypeAndValue.Value"><code>pkix.AttributeTypeAndValue.Value</code></a> 类型；未知类型会被解析为 <a target="_blank" rel="noopener" href="https://pkg.go.dev/encoding/asn1#RawValue"><code>asn1.RawValue</code></a>。</p><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509#Certificate.RawSignatureAlgorithm"><code>Certificate.RawSignatureAlgorithm</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509#CertificateRequest.RawSignatureAlgorithm"><code>CertificateRequest.RawSignatureAlgorithm</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509#RevocationList.RawSignatureAlgorithm"><code>RevocationList.RawSignatureAlgorithm</code></a> 字段，用于暴露签名算法的 DER 编码 AlgorithmIdentifier，即使 <code>SignatureAlgorithm</code> 字段为 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509#UnknownSignatureAlgorithm"><code>UnknownSignatureAlgorithm</code></a> 时也是如此。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509#SystemCertPool"><code>SystemCertPool</code></a> 现在在 Windows 和 Darwin 上也会尊重 <code>SSL_CERT_FILE</code> 和 <code>SSL_CERT_DIR</code>。设置这些环境变量后，会从磁盘加载根证书，并改用 Go 原生验证器，而不再使用平台证书验证 API。该行为可通过 <code>GODEBUG=x509sslcertoverrideplatform=0</code> 关闭。</p><h4 id="crypto-x509-pkix"><a href="#crypto-x509-pkix" class="headerlink" title="crypto/x509/pkix"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/pkix/"><code>crypto/x509/pkix</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/pkix#RDNSequence.String"><code>RDNSequence.String</code></a>（因此也包括 <a target="_blank" rel="noopener" href="https://pkg.go.dev/crypto/x509/pkix#Name.String"><code>Name.String</code></a>）现在即使属性的 OID 无法识别，也会把字符串类型的属性值渲染为字符串。此前这类值总会以其 DER 形式做十六进制编码。参见 <a target="_blank" rel="noopener" href="https://go.dev/issue/33093">#33093</a>。</p><h4 id="database-sql"><a href="#database-sql" class="headerlink" title="database/sql"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/database/sql/"><code>database/sql</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/database/sql#ConvertAssign"><code>ConvertAssign</code></a> 函数，让数据库驱动可以访问 <a target="_blank" rel="noopener" href="https://pkg.go.dev/database/sql#Rows.Scan"><code>Rows.Scan</code></a> 所执行的类型转换。</p><h4 id="database-sql-driver"><a href="#database-sql-driver" class="headerlink" title="database/sql/driver"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/database/sql/driver/"><code>database/sql/driver</code></a></h4><p>驱动可以实现新的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/database/sql/driver#RowsColumnScanner"><code>RowsColumnScanner</code></a> 接口，直接扫描到用户提供的目标值。</p><h4 id="go-constant"><a href="#go-constant" class="headerlink" title="go/constant"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/constant/"><code>go/constant</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/constant#StringLen"><code>StringLen</code></a> 函数，可在不完全构造字符串 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/constant#Value"><code>Value</code></a> 的情况下返回其长度。</p><h4 id="go-scanner"><a href="#go-scanner" class="headerlink" title="go/scanner"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/scanner/"><code>go/scanner</code></a></h4><p>扫描器现在可以通过新的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/scanner#Scanner.End"><code>Scanner.End</code></a> 方法获取 token 的结束位置。</p><h4 id="go-token"><a href="#go-token" class="headerlink" title="go/token"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/token/"><code>go/token</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/token#File"><code>File</code></a> 现在有了 <code>String</code> 方法。</p><h4 id="go-types"><a href="#go-types" class="headerlink" title="go/types"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types/"><code>go/types</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#Hasher"><code>Hasher</code></a> 类型是面向 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#Type"><code>Type</code></a> 的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/hash/maphash#Hasher"><code>maphash.Hasher</code></a> 实现，并遵守 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#Identical"><code>Identical</code></a> 等价关系，从而允许把 <code>Type</code> 用在哈希表及类似数据结构中。<a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#HasherIgnoreTags"><code>HasherIgnoreTags</code></a> 则是对应 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#IdenticalIgnoreTags"><code>IdenticalIgnoreTags</code></a> 的 hasher。</p><p><code>gotypesalias</code> <code>GODEBUG</code> 设置（添加于 <a target="_blank" rel="noopener" href="https://go.dev/doc/godebug#go-122">Go 1.22</a>）已被永久移除；<a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types"><code>go/types</code></a> 包现在无论 <code>GODEBUG</code> 如何设置，都会为<a target="_blank" rel="noopener" href="https://go.dev/ref/spec#Alias_declarations">别名声明</a>生成 <a target="_blank" rel="noopener" href="https://pkg.go.dev/go/types#Alias"><code>Alias</code></a> 类型节点。</p><h4 id="hash-maphash"><a href="#hash-maphash" class="headerlink" title="hash/maphash"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/hash/maphash/"><code>hash/maphash</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/hash/maphash#Hasher"><code>Hasher</code></a> 接口定义了某一类型的值与未来基于哈希的数据结构（例如哈希表和布隆过滤器）之间的约定；参见 <a target="_blank" rel="noopener" href="https://go.dev/issue/70471">#70471</a>。</p><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/hash/maphash#ComparableHasher"><code>ComparableHasher</code></a> 类型为可比较类型提供了便捷的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/hash/maphash#Hasher"><code>Hasher</code></a> 实现，其 <code>Equal</code> 方法定义为 <code>==</code>。</p><h4 id="math-big"><a href="#math-big" class="headerlink" title="math/big"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big/"><code>math/big</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Int"><code>Int</code></a> 现在有了 <a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Int.Divide"><code>Divide</code></a> 方法，用于计算两个 <code>Int</code> 值的商和余数。它支持舍入模式 <a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Trunc"><code>Trunc</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Floor"><code>Floor</code></a>、<a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Round"><code>Round</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/math/big#Ceil"><code>Ceil</code></a>。</p><h4 id="math-rand-v2"><a href="#math-rand-v2" class="headerlink" title="math/rand/v2"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/math/rand/v2/"><code>math/rand/v2</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/math/rand/v2#Rand"><code>Rand</code></a> 现在支持泛型方法 <a target="_blank" rel="noopener" href="https://pkg.go.dev/math/rand/v2#Rand.N"><code>N</code></a>，行为与包级 <a target="_blank" rel="noopener" href="https://pkg.go.dev/math/rand/v2#N"><code>N</code></a> 函数一致。</p><h4 id="net"><a href="#net" class="headerlink" title="net"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/net/"><code>net</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/net#UnixConn"><code>UnixConn</code></a> 的读方法在底层读取返回 EOF 时，现在会直接返回 <a target="_blank" rel="noopener" href="https://pkg.go.dev/io#EOF"><code>io.EOF</code></a>，而不再把它包装进 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net#OpError"><code>net.OpError</code></a>。</p><h4 id="net-http"><a href="#net-http" class="headerlink" title="net/http"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http/"><code>net/http</code></a></h4><p><a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Transport"><code>Transport</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Server"><code>Server</code></a> 现在支持在用户提供的、实现了 <code>ConnectionState() tls.ConnectionState</code> 方法的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net#Conn"><code>net.Conn</code></a> 连接上进行 TLS ALPN 协议协商。</p><p>HTTP&#x2F;2 服务端现在会接受 RFC 9218 定义的客户端优先级信号，从而优先处理更高优先级的 HTTP&#x2F;2 stream。如果希望沿用旧行为（无论优先级如何都以 round-robin 方式处理 stream），可将 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Server.DisableClientPriority"><code>Server.DisableClientPriority</code></a> 设为 <code>true</code>。</p><p>HTTP&#x2F;1 的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Response.Body"><code>Response.Body</code></a> 在关闭时现在会自动排空尚未读取的内容（有一个保守上限），以便更好地复用连接。对大多数程序来说，这一变更应是无操作，或会带来性能提升。在少数情况下，如果程序并不能从连接复用中获益，却错误地让过多空闲连接一直挂着——通常是把 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Transport.MaxIdleConns"><code>Transport.MaxIdleConns</code></a> 设为 <code>0</code>，或对不同请求使用不同的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Client"><code>Client</code></a>，从而绕过 <code>Transport.MaxIdleConns</code> 限制——则可能出现性能下降。此时可将 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Transport.DisableKeepAlives"><code>Transport.DisableKeepAlives</code></a> 设为 <code>true</code> 来关闭连接复用。不过，这类性能下降通常说明 <code>Transport</code> 或 <code>Client</code> 的配置或用法本身就不合理，更深入地检查一下往往会更有帮助。</p><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#Server.MaxHeaderValueCount"><code>Server.MaxHeaderValueCount</code></a> 字段，允许 HTTP 服务端控制愿意接受的 header 值数量。未设置时使用 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http#DefaultMaxHeaderValueCount"><code>DefaultMaxHeaderValueCount</code></a>。</p><h4 id="net-http-httptest"><a href="#net-http-httptest" class="headerlink" title="net/http/httptest"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http/httptest/"><code>net/http/httptest</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http/httptest#NewTestServer"><code>NewTestServer</code></a> 函数，用于创建配置了内存假网络的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/http/httptest#Server"><code>Server</code></a>，适合与 <a target="_blank" rel="noopener" href="https://pkg.go.dev/testing/synctest"><code>testing/synctest</code></a> 包一起使用。</p><h4 id="net-url"><a href="#net-url" class="headerlink" title="net/url"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/net/url/"><code>net/url</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/url#URL.Clone"><code>URL.Clone</code></a> 方法，用于深拷贝 URL。新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/net/url#Values.Clone"><code>Values.Clone</code></a> 方法，用于深拷贝 Values。</p><h4 id="runtime-secret"><a href="#runtime-secret" class="headerlink" title="runtime/secret"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/runtime/secret/"><code>runtime/secret</code></a></h4><p>在<a target="_blank" rel="noopener" href="https://pkg.go.dev/runtime/secret#Do">机密模式</a>下创建的 goroutine，现在会同样在机密模式下执行。</p><h4 id="strings"><a href="#strings" class="headerlink" title="strings"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/strings/"><code>strings</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/strings#CutLast"><code>CutLast</code></a> 函数，按分隔符最后一次出现的位置切分字符串。它可以替代并简化一些常见的 <a target="_blank" rel="noopener" href="https://pkg.go.dev/strings#LastIndex"><code>LastIndex</code></a> 用法。</p><h4 id="syscall"><a href="#syscall" class="headerlink" title="syscall"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/syscall/"><code>syscall</code></a></h4><p>在 Plan 9 上，<a target="_blank" rel="noopener" href="https://pkg.go.dev/syscall#Errno"><code>Errno</code></a> 类型现已定义，并像其他平台一样实现了 <code>error</code> 接口。Plan 9 的系统调用返回的是 <code>ErrorString</code> 值，因此该包在 Plan 9 上永远不会返回 <code>Errno</code>。定义它是为了让引用 <code>syscall.Errno</code> 的可移植代码无需 build constraint 即可在 Plan 9 上构建。</p><h4 id="testing-synctest"><a href="#testing-synctest" class="headerlink" title="testing/synctest"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/testing/synctest/"><code>testing/synctest</code></a></h4><p>新增 <a target="_blank" rel="noopener" href="https://pkg.go.dev/testing/synctest#Sleep"><code>Sleep</code></a> 辅助函数，组合了 <a target="_blank" rel="noopener" href="https://pkg.go.dev/time#Sleep"><code>time.Sleep</code></a> 和 <a target="_blank" rel="noopener" href="https://pkg.go.dev/testing/synctest#Wait"><code>synctest.Wait</code></a>。</p><h4 id="unicode"><a href="#unicode" class="headerlink" title="unicode"></a><a target="_blank" rel="noopener" href="https://pkg.go.dev/unicode/"><code>unicode</code></a></h4><p>unicode 包以及系统中相关支持已从 Unicode 15 升级到 Unicode 17。变更说明见 <a target="_blank" rel="noopener" href="https://www.unicode.org/versions/Unicode16.0.0/">Unicode 16.0.0</a> 和 <a target="_blank" rel="noopener" href="https://www.unicode.org/versions/Unicode17.0.0/">Unicode 17.0.0</a> 发行说明。</p><h2 id="移植"><a href="#移植" class="headerlink" title="移植"></a>移植</h2><h3 id="darwin"><a href="#darwin" class="headerlink" title="Darwin"></a>Darwin</h3><p>正如 Go 1.26 发行说明中所<a target="_blank" rel="noopener" href="https://go.dev/doc/go1.26#darwin">宣布</a>的，Go 1.27 要求 macOS 13 Ventura 或更高版本；对更早版本的支持已经停止。</p><h3 id="powerpc"><a href="#powerpc" class="headerlink" title="PowerPC"></a>PowerPC</h3><p>在 Linux 上的大端 64 位 PowerPC 移植（<code>GOOS=linux</code> <code>GOARCH=ppc64</code>）中，Go 工具链现在会生成使用 ELFv2 系统 ABI 的二进制。ELFv2 支持需要 Linux kernel 3.13 或更高版本。RHEL7 已将该支持回移植到其 3.10 内核。</p><p>现在支持 cgo、位置无关可执行文件（PIE）以及外部链接。使用这些特性需要兼容 ELFv2 的运行时（libc 以及所有被链接和加载的库）。</p><p>对于不使用 cgo 的程序，Go 工具链默认仍会用内部链接生成静态二进制。对于带有 cgo 选项的程序，如果需要静态的纯 Go 二进制，可以在运行 <code>go build</code> 时设置环境变量 <code>CGO_ENABLED=0</code>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>JVM8 结构</title>
      <link>https://yangshifu.xyz/posts/jvm8-specification/</link>
      <description>
        <![CDATA[<p>本篇翻译自 <a target="_blank" rel="noopener" href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html">Oracle 官方文档 —— JVM 规范第二章</a]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/java/">java</category>
      <pubDate>Fri, 21 Aug 2026 19:14:04 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>本篇翻译自 <a target="_blank" rel="noopener" href="https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html">Oracle 官方文档 —— JVM 规范第二章</a>，并包含删减及译者解读。如果不打算深入了解，那么仅需要阅读总结好的 TLDR 部分即可。</p><h2 id="tldr"><a href="#tldr" class="headerlink" title="TLDR"></a>TLDR</h2><p>Java 语言与 JVM 并没有直接联系，它们之间的关联是通过将 Java 源码编译为字节码文件，随后 JVM 执行字节码文件而间接关联的。目前已有多种基于 JVM 平台的开发语言，如 <code>Kotlin</code> <code>Scala</code> 等，它们都可以通过其编译器最终产生字节码文件。也就是说 JVM 的规范是更高一级的虚拟机抽象，不针对任何特定开发语言，只要某种开发语言能正确产生 JVM 能理解的字节码即可。</p><p>在 JVM 规范中，并没有针对运行时的内存布局进行规定，下面的图是 <code>HotSpot</code> 虚拟机在实现 JVM 标准时自由设计的。JVM 的实现，目前常用的是：<code>HotSpot</code> <code>OpenJ9</code> 等，更详细的说明可以参考维基百科。</p><p><img src="/posts/jvm8-specification/images/a243c34352415f5ea7be5ce20c6f2ade.png" alt="jmv8-specification-jmm.png" loading="lazy"></p><p>JVM 规范也没有规定使用何种垃圾回收算法，下表是 <code>HotSpot</code> 虚拟机设计的部分垃圾回收算法及其具体实现的垃圾回收器。</p><table><thead><tr><th align="center">算法</th><th align="center">实现</th></tr></thead><tbody><tr><td align="center">复制</td><td align="center"><code>Serial</code> <code>ParNew</code> <code>Parallel Scavenge</code></td></tr><tr><td align="center">标记整理</td><td align="center"><code>Parallel Old</code></td></tr><tr><td align="center">标记清除</td><td align="center"><code>Concurrent Mark Sweep</code></td></tr><tr><td align="center">其他</td><td align="center"><code>G1</code>宏观基于标记整理，局部基于复制</td></tr></tbody></table><p>JVM 执行的是字节码，而不是 Java 代码，比如以下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-type">int</span> <span class="hljs-title function_">add</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> &#123;<br>  <span class="hljs-keyword">return</span> a + b;<br>&#125;<br></code></pre></td></tr></table></figure><p>将被编译成：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java">ILOAD <span class="hljs-number">1</span><br>ILOAD <span class="hljs-number">2</span><br>IADD<br>IRETURN<br></code></pre></td></tr></table></figure><p>可以看出，在 Java 代码编译之后，其操作数的类型已经确定，并且已经使用了特定类型的指令（ILOAD 中的 I 代表整形）。</p><p><code>boolean</code> <code>byte</code> <code>short</code> <code>char</code> <code>int</code> 几种类型通常都统一使用 <code>int</code> 的指令集来执行操作，比如：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-type">char</span> <span class="hljs-title function_">add_char</span><span class="hljs-params">(<span class="hljs-type">char</span> a, <span class="hljs-type">char</span> b)</span> &#123;<br>   <span class="hljs-keyword">return</span> (<span class="hljs-type">char</span>) (a + b);<br> &#125;<br></code></pre></td></tr></table></figure><p>将被编译成：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java">ILOAD <span class="hljs-number">1</span><br>ILOAD <span class="hljs-number">2</span><br>IADD<br>I2C<br>IRETURN<br></code></pre></td></tr></table></figure><p>与 <code>int</code> 指令的区别仅在于返回时多了一个 <code>I2C</code> 转换指令。</p><p>异常的抛出流程：将沿着调用链一直向外抛出，如果一直没有找到对应的异常处理器，那么抛出异常的线程将被终止执行；如果找到对应的异常处理器，那么使用该处理器处理异常。</p><h2 id="文档正文"><a href="#文档正文" class="headerlink" title="文档正文"></a>文档正文</h2><p>本文档详述了一种抽象的虚拟机，而不是某个虚拟机的具体实现。</p><p>为了正确地实现 JVM，只需要读取 <code>class</code> 字节码文件，然后正确地执行其中指定的操作即可。规范中没有描述到的部分，实现者都可以自由发挥。比如运行时数据的布局、垃圾回收算法的使用及 JVM 指令的内部优化（比如将 JVM 指令直接翻译为机器指令）等，都留给实现者自由决定。</p><h3 id="1-class-文件格式"><a href="#1-class-文件格式" class="headerlink" title="1. class 文件格式"></a>1. <code>class</code> 文件格式</h3><p>编译后被 JVM 用来执行的代码使用了一种硬件及操作系统无关的二进制格式，通常（并不绝对）保存在被称为字节码文件（<code>.class</code>）的文件之中。字节码文件严谨地定义了类或者接口的具体表现，包括平台相关的字节序等细节。</p><h3 id="2-数据类型"><a href="#2-数据类型" class="headerlink" title="2. 数据类型"></a>2. 数据类型</h3><p>与 Java 语言中的数据类型一致，JVM 操作的类型有两类：原始类型和引用类型。相应的，两种数据类型的值都可以被保存在变量中、当做参数传递及当做方法的返回值。</p><p>JVM 期望几乎所有的类型检查都在执行之前完成，通常是编译器，而不是 JVM 自己。在运行时，不需要标记基本类型的值，也不需要对其进行检查以确定它们的类型，或者将它们与引用类型区分开来。相反地，JVM 的指令集使用类型专用的指令来区分其数据类型，比如：<code>iadd</code> <code>ladd</code> <code>fadd</code> <code>dadd</code> 都是 JVM 用来获取两数相加之和的指令，但它们每一个都只针对特定的数据类型：<code>int</code> <code>long</code> <code>float</code> <code>double</code>，下面的内容将会描述这些 JVM 支持的类型及指令。</p><p>JVM 包含了对对象的显式支持。一个对象，要么是动态分配的类实例，要么是一个数组。引用的值可以想象为指向对象的指针，对一个对象可以同时有多个引用。对象总是通过引用的值来进行操作、传递及测试。</p><h3 id="3-原始类型及取值"><a href="#3-原始类型及取值" class="headerlink" title="3. 原始类型及取值"></a>3. 原始类型及取值</h3><p>JVM 支持的原始类型有如下几种：数值类型、布尔类型和 <code>returnAddress</code> 类型。</p><p>数值类型包括整形和浮点型。</p><p>整形包括：</p><ul><li><code>byte</code>， 8 位（8-bit）有符号二进制补码，默认值为 0</li><li><code>short</code>， 16 位（16-bit）有符号二进制补码，默认值为 0</li><li><code>int</code>，32 位（32-bit）有符号二进制补码，默认值为 0</li><li><code>long</code>，64 位（64-bit）有符号二进制补码，默认值为 0</li><li><code>char</code>，16 位（16-bit）无符号整数表示 <code>Unicode 码点</code>，<code>UTF-16</code> 编码，默认值为空码点 <code>\u0000</code></li></ul><p>浮点型包括：</p><ul><li><code>float</code>，单精度浮点数，默认值为 正0</li><li><code>double</code>，双精度浮点数，默认值为 正0</li></ul><p>布尔类型包括 <code>true</code> 和 <code>false</code>，默认值为 <code>false</code>。尽管 JVM 定义了布尔类型，但它只提供了十分有限的支持：没有专门操作布尔类型值的虚拟机指令。相反地，Java 语言中对布尔类型的操作在编译时修改为对 <code>int</code> 的操作。JVM 直接支持了布尔类型数组，<code>newarray</code> 指令可以直接创建布尔数组，对于布尔数组的存取操作则是使用了字节数组（byte array）的指令：<code>baload</code> 和 <code>bastore</code>。在 <code>Oracle</code> 的虚拟机实现（HotSpot）中，布尔数组被编码为了字节数组，数组中的每个布尔元素用一个字节来替代（8 bits）。</p><p><code>returnAddress</code> 类型是一个指向字节码指令的指针，在所有的原始类型中，只有 <code>returnAddress</code> 不能在 Java 语言中直接使用，它只在 JVM 的 <code>jsr</code> <code>ret</code> <code>jsr_w</code> 中使用。</p><h3 id="4-引用类型及取值"><a href="#4-引用类型及取值" class="headerlink" title="4. 引用类型及取值"></a>4. 引用类型及取值</h3><p>引用类型有三种：类类型、数组类型和接口类型。他们的值分别指向了动态创建的类实例、数组实例或实现接口的类实例及实现接口的数组实例。</p><p>数组类型包含了一个单一维度的组件类型，称为数组的元素类型。一个数组的元素类型必须为原始类型、类类型或接口类型中的一种。</p><p>引用类型的值还可以是特殊的 <code>null</code>，表示不指向任何对象。<code>null</code> 引用没有运行时的类型，但是可以转换为任何类型，其默认值为 <code>null</code>。JVM 规范没有强制指定 <code>null</code> 的值。</p><h3 id="5-运行时数据区"><a href="#5-运行时数据区" class="headerlink" title="5. 运行时数据区"></a>5. 运行时数据区</h3><p>JVM 为一个程序的执行过程定义了许多运行时数据区，其中一部分在 JVM 启动时创建在 JVM 退出时才销毁，其他数据区域则是线程专属的，随线程而生，随线程而亡。</p><h4 id="5-1-pc-寄存器"><a href="#5-1-pc-寄存器" class="headerlink" title="5.1 pc 寄存器"></a>5.1 <code>pc</code> 寄存器</h4><p>JVM 支持多线程同时执行，每个线程都有其专属的 <code>pc</code>（Program Counter） 寄存器。在任意时间，每个线程都在执行一个单独的方法，也就是该线程的<strong>当前方法</strong>。如果该方法不是 <code>native</code> 的本地方法，那么 <code>pc</code> 寄存器中保存了当前执行的指令的地址；如果该方法是 <code>native</code> 的本地方法，那么 <code>pc</code> 寄存器为空。<code>pc</code> 寄存器的范围足够在任意的系统平台上存放一个 <code>returnAddress</code> 或本地指针（<code>native pointer</code>）。</p><h4 id="5-2-jvm-栈"><a href="#5-2-jvm-栈" class="headerlink" title="5.2 JVM 栈"></a>5.2 JVM 栈</h4><p>每个 JVM 线程在创建时都伴随了一个私有的 JVM 栈，JVM 栈保存栈帧。JVM 栈和其他语言中的栈一样，它保存本地变量和部分返回值，在方法的执行和返回中承担作用。因为 JVM 栈除了 <code>push</code> 和 <code>pop</code> 以外不会直接操作，所以它有可能被分配在堆（Heap）上，栈内存空间也不必是连续的。</p><p>JVM 规范允许 JVM 栈数量可以为固定大小或者动态伸缩，有以下异常条件与之关联：</p><ul><li>固定大小：如果一个线程需要的栈空间超过了 JVM 允许的大小，则 JVM 抛出 <code>StackOverflowError</code> 错误；</li><li>动态伸缩：如果 JVM 无法满足一个线程所需的栈空间，则 JVM 抛出 <code>OutOfMemoryError</code> 错误。</li></ul><h4 id="5-3-堆"><a href="#5-3-堆" class="headerlink" title="5.3 堆"></a>5.3 堆</h4><p>JVM 在启动时创建一个所有线程共享的堆（Heap）空间，所有的类实例和数组都分配在其中。堆空间由 <code>自动存储管理系统</code>（通常是 <code>Garbage Collector</code> 即垃圾回收器）回收，其中的对象无法显式地销毁。堆空间可以不连续。</p><p>JVM 的实现需要提供可以让使用者进行设置的堆初始大小参数，如果堆可以动态伸缩，还必须提供堆的上下限大小参数。</p><p>如果 <code>自动存储管理系统</code> 无法提供足够的空间时，JVM 抛出 <code>OutOfMemoryError</code> 错误。</p><h4 id="5-4-方法区"><a href="#5-4-方法区" class="headerlink" title="5.4 方法区"></a>5.4 方法区</h4><p>JVM 在启动时创建一个所有虚拟机线程共享的方法区，它类似于其他编译型语言生成的 <code>text</code> 段。在其中保存了每个类各自的结构，包括：运行时常量池、字段、方法数据、构造器、特殊方法（§9）以及方法中的指令。</p><p>尽管方法区是堆的一个逻辑部分，简单的实现可以选择不进行垃圾回收或者压缩该部分。JVM 规范没有强制指定方法区的位置，以及如何管理那些编译后的代码。方法区与堆一样，可以为固定大小或动态伸缩，且空间也可以不连续。</p><p>JVM 的实现需要提供可以让使用者进行设置的方法区初始化大小，如果方法区可以动态伸缩，还必须提供方法区上下限大小参数。</p><p>如果该区域运行时请求的大小无法满足，则 JVM 抛出 <code>OutOfMemoryError</code> 错误。</p><h4 id="5-5-运行时常量池"><a href="#5-5-运行时常量池" class="headerlink" title="5.5 运行时常量池"></a>5.5 运行时常量池</h4><p>运行时常量池是每个类或接口用来存放字节码文件中定义的 <code>constant_poll</code> 表的区域，其中包含了几种类型的常量，范围从编译时已知的数值字面量到运行时方法和字段的引用。运行时常量池类似于其他编译型语言中的符号表（<code>Symbol Table</code>）。</p><p>每个运行时常量池都分配在 JVM 的方法区，而且在类或接口被 JVM 创建时初始化。</p><p>当创建一个类或接口时，方法区无法满足创建它的运行时常量池所需空间时，JVM 抛出 <code>OutOfMemoryError</code> 错误。</p><p>在第五章将描述运行时常量池的初始化。</p><h4 id="5-6-本地方法栈"><a href="#5-6-本地方法栈" class="headerlink" title="5.6 本地方法栈"></a>5.6 本地方法栈</h4><p>JVM 的实现可能会使用到传统的栈，即 <code>C栈</code>，来支持本地方法（使用 Java 以外的语言写的方法）的执行，当 JVM 的实现用其他语言（如 <code>C</code>）来实现时，也会用到本地方法栈。不能加载本机方法且本身不依赖于传统堆栈的Java虚拟机实现不需要提供本地方法堆栈。如果提供，则通常在创建每个线程时为每个线程分配本地方法堆栈。</p><p>同 JVM 栈一样，本地方法栈允许为固定大小或者动态伸缩，有以下异常条件与之关联：</p><ul><li>固定大小：如果一个线程需要的本地方法栈空间超过了 JVM 允许的大小，则 JVM 抛出 <code>StackOverflowError</code> 错误；</li><li>动态伸缩：如果 JVM 无法满足一个线程所需的栈空间，则 JVM 抛出 <code>OutOfMemoryError</code> 错误。</li></ul><h3 id="6-栈帧"><a href="#6-栈帧" class="headerlink" title="6. 栈帧"></a>6. 栈帧</h3><p>栈帧用于保存数据和部分结果，也用于执行动态链接（Perform dynamic linking）、为方法返回值（Return values for method）和分发异常（Dispatch exception）。</p><p>栈帧在方法执行时创建，在方法执行结束时销毁，无论是正常结束还是异常结束（方法抛出了未捕获的异常）。创建栈帧的线程将栈帧分配到了该线程所对应的 JVM 栈中，也就是线程私有的。每个栈帧都有专属的局部变量表、专属的操作数栈，和一个当前方法所属类中的运行时常量池。</p><p>局部变量表和操作数栈各自的大小在编译时就已经确定，栈帧关联方法的 <code>Code</code> 属性也是同样。因此栈帧数据结构的大小仅与具体的 JVM 实现有关，这些结构的内存可以在方法调用中同时分配。</p><p>在一个线程之中，只有当前正在执行的方法所对应的栈帧是活动的，这个栈帧被称为<strong>当前帧</strong>，正在执行的方法被称为<strong>当前方法</strong>，方法所处的类被称为<strong>当前类</strong>。对局部变量表和操作数栈的操作仅针对于当前帧。</p><p>如果当前栈帧对应的方法调用了其他方法，或者当前栈帧执行结束，那么它就不再是<strong>当前帧</strong>了。当一个新的方法被调用，一个新的栈帧就被创建出来，控制权移交给新方法的同时，<strong>当前帧</strong>的概念也一并移交了。在方法返回时，<strong>当前帧</strong>将回传方法的结果到<strong>前一个栈帧</strong>，如果存在<strong>前一个栈帧</strong>的话，接下来<strong>当前帧</strong>被丢弃，<strong>前一个栈帧</strong>变为<strong>当前帧</strong>。</p><p>注意，线程创建的栈帧为其私有的数据，无法被其他线程引用。</p><h4 id="6-1-局部变量表"><a href="#6-1-局部变量表" class="headerlink" title="6.1 局部变量表"></a>6.1 局部变量表</h4><p>每个栈帧都包含一个被称为局部变量表的变量数组，它的长度在编译时就已经确定，并存储在字节码文件中类或接口的方法定义的 <code>Code</code> 属性中。</p><p>单个的局部变量可以保存 <code>boolean</code> <code>byte</code> <code>char</code> <code>short</code> <code>int</code> <code>float</code> <code>refrence</code> <code>returnAddress</code> 类型的值，一对局部变量可以保存 <code>long</code> 或者 <code>double</code> 类型的值。</p><p>局部变量表是索引寻址的，其索引范围从 0 到局部变量表大小。<code>long</code> 或者 <code>double</code> 类型的值占用了两个连续的局部变量，这样的值只能用<strong>较小的</strong>索引来访问。比如，一个 <code>double</code> 变量的值占用了 <code>n</code> 和 <code>n+1</code> 的位置，在此情况下，索引 <code>n+1</code> 无法被单独读取，但是却可以写入。写入将导致索引 <code>n</code> 的值无效。</p><p>JVM 并不要求 <code>n</code> 一定为偶数，从直观的角度来看，<code>long</code> 和 <code>double</code> 类型的值不需要在局部变量表中对齐 64 位，实现者可以自由地采取一个合适的方法。</p><p>JVM 在方法调用时使用局部变量来接收参数。当一个类方法被调用时，所有的局部变量被连续地接收参数传入；在类实例方法调用时，索引为 0 的局部变量总是指向当前被调用的对象实例（也就是 Java 语言中的 <code>this</code>）。</p><h4 id="6-2-操作数栈"><a href="#6-2-操作数栈" class="headerlink" title="6.2 操作数栈"></a>6.2 操作数栈</h4><p>每个栈帧都包含一个被称为操作数栈的后进先出队列。与局部变量表类似，操作数栈的深度在编译时就已经确定。</p><p>在明确上下文的情况下，我们简称当前栈帧的操作数栈为操作数栈。</p><p>在栈帧创建时，操作数栈初始化为空。JVM 提供了指令来读取常量、局部变量或实例字段到操作数栈中。还有一些 JVM 指令从操作数栈获取操作数，执行操作，然后将执行结果压回操作数栈中。操作数栈同样也用于准备传递到方法的参数和接收方法的返回值。</p><p>比如：<code>iadd</code> 指令使得两个 <code>int</code> 值相加，它要求准备进行相加操作的两个 <code>int</code> 值处于操作数栈的顶部，两个 <code>int</code> 值都被弹出（pop），相加后将它们的和压回操作数栈中。</p><p>操作数栈中的每一项都可以保存任何 JVM 中的类型，包括 <code>long</code> 和 <code>double</code> 类型。</p><p>操作数栈中的值必须按照它们对应的类型来进行操作，例如，不可能将两个 <code>int</code> 值压入操作数栈中，然后将它们当做 <code>long</code> 来处理，也不能将两个 <code>float</code> 值压入操作数栈中，然后用 <code>iadd</code> 指令将它们相加。少数 JVM 指令（<code>dup</code> <code>swap</code>）在运行时数据区域执行操作时忽略其所操作的值的类型，这些指令被定义为无法修改或拆分单个值。这些对操作数栈的操作限制是在 <code>class</code> 文件验证时实现的。</p><p>在任何时间点，操作数栈都有一个关联的深度，其中 <code>long</code> 和 <code>double</code> 占用两个单位的深度，其他值占用一个。</p><h4 id="6-3-动态链接"><a href="#6-3-动态链接" class="headerlink" title="6.3 动态链接"></a>6.3 动态链接</h4><p>为了支持方法代码的动态链接，每个栈帧都包含了一个对当前方法运行时常量池（§5.5）的引用。一个方法的字节码通过符号引用指向了被调用的方法和被访问的变量。动态链接翻译这些符号引用到实际的方法引用，在遇到未知的符号时加载对应的类，然后将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移。</p><p>方法和变量的后期绑定使得方法调用的其他类在进行更改时不会本地的方法和变量造成影响。</p><h4 id="6-4-正常方法调用结束"><a href="#6-4-正常方法调用结束" class="headerlink" title="6.4 正常方法调用结束"></a>6.4 正常方法调用结束</h4><p>正常方法调用结束指的是，在调用过程中，没有直接从 JVM 抛出或从代码中显式抛出异常。当前方法正常调用结束时，将返回值返回给调用它的方法。当被调用的方法执行一个返回指令（§11.8）时发生这种情况，返回指令的选择必须适合被返回的值的类型(如果有的话)。</p><p>当前帧用来恢复调用者的状态，包括它的局部变量和操作数栈，然后 <code>pc</code> 寄存器适当递增，跳过刚刚执行的方法指令。随后，程序获取到方法返回值并压入操作数栈后继续执行。</p><h4 id="6-5-异常方法调用结束"><a href="#6-5-异常方法调用结束" class="headerlink" title="6.5 异常方法调用结束"></a>6.5 异常方法调用结束</h4><p>异常方法调用结束指的是在方法执行过程中 JVM 抛出了该方法没有进行处理的异常。执行 <code>athrow</code> 指令还会导致显式抛出异常，如果该异常未被当前方法捕获，则会导致方法调用的突然结束。突然结束的方法调用永远不会将值返回给其调用者。</p><h3 id="7-对象的表示"><a href="#7-对象的表示" class="headerlink" title="7 对象的表示"></a>7 对象的表示</h3><p>JVM 没有强制要求对象特定的内部结构。</p><p>在一些 Oracle 实现的 JVM 中，对类实例的引用是指向句柄的指针，而句柄本身就是一对指针：其中一个指向了包含对象方法和对象类型的表，另一个指向了对象实际分配的堆内存地址。</p><h3 id="8-浮点运算"><a href="#8-浮点运算" class="headerlink" title="8 浮点运算"></a>8 浮点运算</h3><p>（浮点运算对于普通开发者来说理解起来过于艰难，暂时忽略）</p><h3 id="9-特殊方法"><a href="#9-特殊方法" class="headerlink" title="9 特殊方法"></a>9 特殊方法</h3><p>在 JVM 层面，每个 Java 语言中的构造函数都显示为一个特殊的<strong>实例初始化方法</strong>：<code>&lt;init&gt;</code>，这个名称是由编译器指定的。这个方法无法被 Java 语言直接使用，因为<code>&lt;init&gt;</code>不是一个有效的标识符。它只能在 JVM 内部通过 <code>invokespecial</code> 指令来调用，且只能在未初始化的类实例上调用。实例初始化方法与其派生的构造函数的访问权限一致。<br>一个类或接口最多有一个<strong>类或接口初始化方法</strong>，而且它们的初始化都必须通过这个方法的调用来进行。类或接口的<strong>类或接口初始化方法</strong>名称均为 <code>&lt;clinit&gt;</code> 且没有参数和返回值。</p><blockquote><p>实例初始化方法<init>和类初始化方法的执行时机不同，<code>init</code> 在实例化一个对象时调用，而 <code>clinit</code> 在 JVM 类加载器加在该类时调用。</p></blockquote><p>在 51.0 以后的字节码版本中，<strong>类或接口初始化方法</strong>必须以 <code>ACC_STATIC</code> 标志进行标记。</p><blockquote><p>即 Java SE 1.7 以后，<clinit> 方法必须为 <code>static</code></p></blockquote><p><code>&lt;clinit&gt;</code> 方法同样由编译器提供，同样也不能显示地在 Java 程序中调用，甚至不会在 JVM 中由指令进行调用；它仅在类加载器加载该类时执行。</p><h3 id="10-异常"><a href="#10-异常" class="headerlink" title="10 异常"></a>10 异常</h3><p>异常在 JVM 中的表现形式为<strong>实现了 <code>Throwable</code> 接口的类或其子类的实例</strong>。<br>抛出异常将会立即引发控制权非本地转移，即从异常抛出点转移到异常处理点。</p><p>大部分异常都是当前线程执行操作的结果，它们是同步的。相对的，异步的异常可能在程序执行的任何时候抛出。JVM 抛出异常有以下三个原因：</p><ul><li>显式执行 <code>athrow</code> 指令</li><li>JVM 同步检测到异常的执行条件，这些异常仅在以下情况抛出，而不会随意抛出：<ul><li>程序的执行可能会发生异常：<ul><li>当一个操作指令违反 Java 的语义，比如数组的越界访问；</li><li>当加载或链接时出现错误。</li></ul></li><li>导致超过资源的某些限制，比如使用了过多的内存。</li></ul></li><li>异步的异常由以下情况触发：<ul><li><code>Thread</code> 或 <code>ThreadGroup</code> 的 <code>stop</code> 方法被调用；</li><li>JVM 的实现出现了内部错误。<br><code>stop</code> 方法会影响其他线程或者某个线程组里的所有线程，因为它们会在其他线程中的任意时间抛出。JVM 内部错误也被认为是一种异步异常。</li></ul></li></ul><p>JVM 可能允许在异步异常抛出之前继续进行少量的操作。</p><blockquote><p>由于指令重排，可能会导致某些本来顺序排在异常抛出点后的代码被提前执行。<br>在遵守 Java 语义的情况下，这样的延迟是被允许的，可以让优化后的代码在实际的执行点去检测并抛出异常。</p></blockquote><p>JVM 抛出的异常是明确的：当控制转移发生时，在抛出异常之前执行的所有指令都必须生效。在异常抛出之后不会再有指令被执行。在异常发生时，一些原本处于异常点之后的代码由于优化重排在异常点之前就执行了，那这些优化后的代码必须保证它们的执行在用户层面不可见。</p><p>JVM 的中每个方法都有 0 个或多个异常处理器与之关联。异常处理器通过字节码偏移指定了其在方法中的有效范围，描述了其所对应的异常类型，以及指定了处理异常的代码位置。异常处理器与异常的匹配，需要同时检查异常抛出的位置是否在处理器的偏移范围之内，以及类型是否一致或为声明的子类。当抛出异常时，JVM 在当前方法中搜索一个匹配的异常处理器，如果找到，则将控制权转到对应的处理器代码之中。如果没有找到，那么当前方法异常结束（§6.5）。在异常结束时，当前方法的操作数栈和局部变量表都被直接丢弃，当前栈帧被直接弹出，并恢复调用方的栈帧。随后异常在调用方中再次被抛出，继续搜索当前方法中匹配的异常处理，如果没有找到，那就继续弹栈、恢复，继续抛出，直到有对应的异常处理器对它进行处理。如果在到达方法调用链顶部之前没有找到合适的异常处理程序，则终止抛出异常的线程的执行。</p><p>在异常处理器中搜索匹配的处理器的顺序非常重要。在一个 <code>class</code> 字节码文件里，异常处理器被保存在一个表中。在运行时，当异常被抛出时，JVM 按顺序从头到尾搜索合适的异常处理器。</p><p>注意，JVM 并不强制对方法的异常表条目进行嵌套或排序。Java 的异常处理语义只有与编译器结合才能实现。当以其他方式生成字节码文件时，确保搜索过程与 JVM 实现的行为一致。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Java 指令集概述</title>
      <link>https://yangshifu.xyz/posts/java-instruction-set-summary/</link>
      <description>
        <![CDATA[<p>一个 JVM 指令由两部分组成，第一部分是一个字节（one-byte）的操作码，第二部分是 0 个或多个提供参数或数据的操作数，许多指令都只有第一部分。</p>]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/java/">java</category>
      <pubDate>Fri, 21 Aug 2026 19:05:15 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>一个 JVM 指令由两部分组成，第一部分是一个字节（one-byte）的操作码，第二部分是 0 个或多个提供参数或数据的操作数，许多指令都只有第一部分。</p><span id="more"></span><p>在忽略异常处理的情况下，JVM 解释器通常是在做如下循环：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">do</span> &#123;<br>    自动计算 PC 寄存器,并从 PC 寄存器获取操作码<br>    如果还有操作数,那么获取操作数<br>    执行操作<br>&#125; <span class="hljs-keyword">while</span> (还有更多可以做);<br></code></pre></td></tr></table></figure><p>操作数的数量由操作码决定。如果一个操作数的大小大于一个字节，那么使用大端序保存。字节码指令流是单字节（single-byte）对齐的。有两个指令例外：<code>lookupswitch</code> 和 <code>tableswitch</code>，它们被强制为 4 字节（4-byte）对齐。</p><h2 id="1-类型与-jvm"><a href="#1-类型与-jvm" class="headerlink" title="1. 类型与 JVM"></a>1. 类型与 JVM</h2><p>JVM 指令集中的大部分指令编码与它们执行的操作数据类型有关，如： <code>iload</code> 指令读取局部变量的 <code>int</code> 值并压入操作数栈中。<code>fload</code> 指令对 <code>float</code> 类型做了同样的动作。两个指令实现了同样的功能，但是操作码却不同。</p><p>对于多数类型指定的指令，指令的类型已经在操作码的字面定义中体现：<code>i</code> 对应 <code>int</code> 操作，<code>l</code> 对应 <code>long</code>，<code>s</code> 对应 <code>short</code>，<code>b</code> 对应 <code>byte</code>，<code>c</code> 对应 <code>char</code>，<code>f</code> 对应 <code>float</code>，<code>d</code> 对应 <code>double</code>，<code>a</code> 对应引用类型。某些类型的指令明确说明类型不在命名中体现，比如 <code>arraylength</code> 指令始终对一个数组对象进行操作。其他一些指令，例如 <code>goto</code> ，一个无条件控制转移，不对类型指定的操作数进行操作。</p><p>限定操作数只为一个字节的大小，对 JVM 的指令集的设计也带来了压力。如果每个类型化指令都支持所有Java虚拟机的运行时数据类型，那么就会有比一个字节所能表示的更多的指令。相反，JVM 的指令集为某些操作提供了减少的类型支持。换句话说，指令集是故意不正交的。根据需要，可以使用单独的指令在不支持的和受支持的数据类型之间进行转换。</p><p>下面的表格总结了 JVM 指令集支持的类型，如果有些类型对应的操作码是空的，则说明没有支持该类型的指令。比如：<code>int</code> 类型有 <code>iload</code> 指令，而 <code>byte</code> 类型却没有对应的指令。请注意，下面表格中很多操作码都没有与 <code>byte</code> <code>char</code> <code>short</code> 相关的对应，甚至 <code>boolean</code> 类型都没有在表格里。编译器将 <code>byte</code> 和 <code>short</code> 类型通过符号扩展成为了 <code>int</code>，<code>boolean</code> 和 <code>char</code> 通过零扩展成为了 <code>int</code>。也就是说，<code>boolean</code> <code>byte</code> <code>char</code> <code>short</code> 的大多数操作都被当做 <code>int</code> 来对待。</p><blockquote><p>符号扩展：低位数转高位数，在低位数的左边补上低位数的符号位，比如将一个字节扩展为两个字节：0b0000_1010 通过符号扩展成为 0b0000_0000_0000_1010，0b1000_0001 通过符号扩展成为 0b1111_1111_1000_0001。</p></blockquote><blockquote><p>零扩展：低位数转高位数，在低位数的左边全补 0，比如：0b0000_1010 通过零扩展成为 0b0000_0000_0000_1010，0b1000_0001 通过零扩展成为 0b0000_0000_1000_0001。</p></blockquote><table><thead><tr><th>opcode</th><th><code>byte</code></th><th><code>short</code></th><th><code>int</code></th><th><code>long</code></th><th><code>float</code></th><th><code>double</code></th><th><code>char</code></th><th><code>reference</code></th></tr></thead><tbody><tr><td><em>Tipush</em></td><td><em>bipush</em></td><td><em>sipush</em></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tconst</em></td><td></td><td></td><td><em>iconst</em></td><td><em>lconst</em></td><td><em>fconst</em></td><td><em>dconst</em></td><td></td><td><em>aconst</em></td></tr><tr><td><em>Tload</em></td><td></td><td></td><td><em>iload</em></td><td><em>lload</em></td><td><em>fload</em></td><td><em>dload</em></td><td></td><td><em>aload</em></td></tr><tr><td><em>Tstore</em></td><td></td><td></td><td><em>istore</em></td><td><em>lstore</em></td><td><em>fstore</em></td><td><em>dstore</em></td><td></td><td><em>astore</em></td></tr><tr><td><em>Tinc</em></td><td></td><td></td><td><em>iinc</em></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Taload</em></td><td><em>baload</em></td><td><em>saload</em></td><td><em>iaload</em></td><td><em>laload</em></td><td><em>faload</em></td><td><em>daload</em></td><td><em>caload</em></td><td><em>aaload</em></td></tr><tr><td><em>Tastore</em></td><td><em>bastore</em></td><td><em>sastore</em></td><td><em>iastore</em></td><td><em>lastore</em></td><td><em>fastore</em></td><td><em>dastore</em></td><td><em>castore</em></td><td><em>aastore</em></td></tr><tr><td><em>Tadd</em></td><td></td><td></td><td><em>iadd</em></td><td><em>ladd</em></td><td><em>fadd</em></td><td><em>dadd</em></td><td></td><td></td></tr><tr><td><em>Tsub</em></td><td></td><td></td><td><em>isub</em></td><td><em>lsub</em></td><td><em>fsub</em></td><td><em>dsub</em></td><td></td><td></td></tr><tr><td><em>Tmul</em></td><td></td><td></td><td><em>imul</em></td><td><em>lmul</em></td><td><em>fmul</em></td><td><em>dmul</em></td><td></td><td></td></tr><tr><td><em>Tdiv</em></td><td></td><td></td><td><em>idiv</em></td><td><em>ldiv</em></td><td><em>fdiv</em></td><td><em>ddiv</em></td><td></td><td></td></tr><tr><td><em>Trem</em></td><td></td><td></td><td><em>irem</em></td><td><em>lrem</em></td><td><em>frem</em></td><td><em>drem</em></td><td></td><td></td></tr><tr><td><em>Tneg</em></td><td></td><td></td><td><em>ineg</em></td><td><em>lneg</em></td><td><em>fneg</em></td><td><em>dneg</em></td><td></td><td></td></tr><tr><td><em>Tshl</em></td><td></td><td></td><td><em>ishl</em></td><td><em>lshl</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tshr</em></td><td></td><td></td><td><em>ishr</em></td><td><em>lshr</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tushr</em></td><td></td><td></td><td><em>iushr</em></td><td><em>lushr</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tand</em></td><td></td><td></td><td><em>iand</em></td><td><em>land</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tor</em></td><td></td><td></td><td><em>ior</em></td><td><em>lor</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Txor</em></td><td></td><td></td><td><em>ixor</em></td><td><em>lxor</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>i2T</em></td><td><em>i2b</em></td><td><em>i2s</em></td><td></td><td><em>i2l</em></td><td><em>i2f</em></td><td><em>i2d</em></td><td></td><td></td></tr><tr><td><em>l2T</em></td><td></td><td></td><td><em>l2i</em></td><td></td><td><em>l2f</em></td><td><em>l2d</em></td><td></td><td></td></tr><tr><td><em>f2T</em></td><td></td><td></td><td><em>f2i</em></td><td><em>f2l</em></td><td></td><td><em>f2d</em></td><td></td><td></td></tr><tr><td><em>d2T</em></td><td></td><td></td><td><em>d2i</em></td><td><em>d2l</em></td><td><em>d2f</em></td><td></td><td></td><td></td></tr><tr><td><em>Tcmp</em></td><td></td><td></td><td></td><td><em>lcmp</em></td><td></td><td></td><td></td><td></td></tr><tr><td><em>Tcmpl</em></td><td></td><td></td><td></td><td></td><td><em>fcmpl</em></td><td><em>dcmpl</em></td><td></td><td></td></tr><tr><td><em>Tcmpg</em></td><td></td><td></td><td></td><td></td><td><em>fcmpg</em></td><td><em>dcmpg</em></td><td></td><td></td></tr><tr><td><em>if_TcmpOP</em></td><td></td><td></td><td><em>if_icmpOP</em></td><td></td><td></td><td></td><td></td><td><em>if_acmpOP</em></td></tr><tr><td><em>Treturn</em></td><td></td><td></td><td><em>ireturn</em></td><td><em>lreturn</em></td><td><em>freturn</em></td><td><em>dreturn</em></td><td></td><td><em>areturn</em></td></tr></tbody></table><p>某些 JVM 指令并不在意类型，比如 <code>pop</code> 和 <code>swap</code> 指令，它们直接操作操作数栈，而无论栈中的数据类型如何。</p><h2 id="2-加载和存储指令"><a href="#2-加载和存储指令" class="headerlink" title="2. 加载和存储指令"></a>2. 加载和存储指令</h2><p>加载和存储指令在栈帧里的局部变量与操作数栈之间进行传递：</p><ul><li>加载一个局部变量到操作数栈：<code>iload</code> <code>iload_&lt;n&gt;</code> <code>lload</code> <code>lload_&lt;n&gt;</code> <code>fload</code> <code>float_&lt;n&gt;</code> <code>dload</code> <code>dload_&lt;n&gt;</code> <code>aload</code> <code>aload_&lt;n&gt;</code></li><li>将操作数栈中的值保存到局部变量中：<code>istore</code> <code>istore_&lt;n&gt;</code> <code>lstore</code> <code>lstore_&lt;n&gt;</code> <code>fstore</code> <code>fstore_&lt;n&gt;</code> <code>dstore</code> <code>dstore_&lt;n&gt;</code> <code>astore</code> <code>astore_&lt;n&gt;</code></li><li>将一个常量加载到操作数栈：<code>bipush</code> <code>sipush</code> <code>ldc</code> <code>ldc_w</code> <code>ldc2_w</code> <code>aconst_null</code> <code>iconst_m1</code> <code>iconst_&lt;i&gt;</code> <code>lconst_&lt;i&gt;</code> <code>fconst_&lt;f&gt;</code> <code>dconst_&lt;d&gt;</code></li><li>使用更宽的索引访问更多的局部变量，或者访问更大的直接操作数：<code>wide</code></li></ul><blockquote><p><code>iload_0</code> 和 <code>iload</code> 的意思都是是将局部变量表中的下标 0 条目加载到操作数栈，<code>istore_2</code> 的意思是将操作数栈顶元素弹出并保存到局部变量表下标为 2 的条目。</p></blockquote><h2 id="3-计算指令"><a href="#3-计算指令" class="headerlink" title="3. 计算指令"></a>3. 计算指令</h2><p>计算指令弹出并计算操作数栈栈顶的两个值，然后将结果压回操作数栈，包括如下指令：</p><ul><li>加：<code>iadd</code> <code>ladd</code> <code>fadd</code> <code>dadd</code></li><li>减：<code>isub</code> <code>lsub</code> <code>fsub</code> <code>dsub</code></li><li>乘：<code>imul</code> <code>lmul</code> <code>fmul</code> <code>dmul</code></li><li>除：<code>idiv</code> <code>ldiv</code> <code>fdiv</code> <code>ddiv</code></li><li>求余：<code>irem</code> <code>lrem</code> <code>frem</code> <code>drem</code></li><li>取反：<code>ineg</code> <code>lneg</code> <code>fneg</code> <code>dneg</code></li><li>位移：<code>ishl</code> <code>ishr</code> <code>iushr</code> <code>lshl</code> <code>lshr</code> <code>lushr</code></li><li>按位或：<code>ior</code> <code>lor</code></li><li>按位与：<code>iand</code> <code>land</code></li><li>按位异或：<code>ixor</code> <code>lxor</code></li><li>局部变量递增：<code>iinc</code></li><li>比较：<code>dcmpg</code> <code>dcmpl</code> <code>fcmpg</code> <code>fcmpl</code> <code>lcmp</code></li></ul><h2 id="4-类型转换指令"><a href="#4-类型转换指令" class="headerlink" title="4. 类型转换指令"></a>4. 类型转换指令</h2><p>类型转换指令允许 JVM 的两个数字类型进行互转，包括以下：</p><ul><li>低位数转高位数：<ul><li><code>int</code> 转 <code>long</code> 或 <code>float</code> 或 <code>double</code>：<code>i2l</code> <code>i2f</code> <code>i2d</code></li><li><code>long</code> 转 <code>float</code> 或 <code>double</code>：<code>l2f</code> <code>l2d</code></li><li><code>float</code> 转 <code>double</code>：<code>f2d</code></li></ul></li><li>高位数转低位数：<ul><li><code>int</code> 转 <code>byte</code> <code>short</code> <code>char</code>：<code>i2b</code> <code>i2s</code> <code>i2c</code></li><li><code>long</code> 转 <code>int</code>：<code>l2i</code></li><li><code>float</code> 转 <code>int</code> <code>long</code>：<code>f2i</code> <code>f2l</code></li><li><code>double</code> 转 <code>int</code> <code>long</code> <code>float</code>：<code>d2i</code> <code>d2l</code> <code>d2f</code></li></ul></li></ul><h2 id="5-对象的创建与操作"><a href="#5-对象的创建与操作" class="headerlink" title="5. 对象的创建与操作"></a>5. 对象的创建与操作</h2><p>虽然类实例和数组都是对象，但是 JVM 创建和操作它们的时候使用了不同的指令集：</p><ul><li>创建类实例：<code>new</code></li><li>创建数组：<code>newarray</code> <code>anewarray</code> <code>multianewarray</code></li><li>访问类属性（static field）：<code>getstatic</code> <code>putstatic</code></li><li>访问成员属性：<code>getfield</code> <code>putfield</code></li><li>加载数组中的元素到操作数栈：<code>baload</code> <code>caload</code> <code>saload</code> <code>iaload</code> <code>laload</code> <code>faload</code> <code>daload</code> <code>aaload</code></li><li>将操作数栈元素保存到数组中某个元素：<code>bastore</code> <code>castore</code> <code>sastore</code> <code>iastore</code> <code>lastore</code> <code>fastore</code> <code>dastore</code> <code>aastore</code></li><li>获取数组的长度：<code>arraylength</code></li><li>检查实例类型：<code>instanceof</code> <code>checkcast</code></li></ul><h2 id="6-操作数栈管理指令"><a href="#6-操作数栈管理指令" class="headerlink" title="6. 操作数栈管理指令"></a>6. 操作数栈管理指令</h2><p><code>pop</code> <code>pop2</code> <code>dup</code> <code>dup2</code> <code>dup_x1</code> <code>dup2_x1</code> <code>dup_x2</code> <code>dup2_x2</code> <code>swap</code></p><h2 id="7-控制转移指令"><a href="#7-控制转移指令" class="headerlink" title="7. 控制转移指令"></a>7. 控制转移指令</h2><p>控制转移指令将使 JVM 的执行转向对应的指令，而不是控制转移指令之后的指令：</p><ul><li>条件转移：<code>ifeq</code> <code>ifne</code> <code>iflt</code> <code>ifle</code> <code>ifgt</code> <code>ifge</code> <code>ifnull</code> <code>ifnonnull</code> <code>if_icmpeq</code> <code>if_icmpne</code> <code>if_icmplt</code> <code>if_icmple</code> <code>if_icmpgt</code> <code>if_icmpge</code> <code>if_acmpeq</code> <code>if_acmpne</code></li><li>符合条件分支：<code>tableswitch</code> <code>lookupswitch</code></li><li>无条件转移：<code>goto</code> <code>goto_w</code> <code>jsr</code> <code>jsr_w</code> <code>ret</code></li></ul><p>JVM 在条件转移指令集中区分了 <code>int</code> 和引用类型。<code>boolean</code> <code>byte</code> <code>char</code> <code>short</code> 使用 <code>int</code> 指令集进行比较。<code>long</code> <code>float</code> <code>double</code> 的比较使用《3.计算指令》中描述的比较指令先得到一个 <code>int</code> 结果，再根据 <code>int</code> 结果的分支进行跳转。</p><h2 id="8-方法调用和返回指令"><a href="#8-方法调用和返回指令" class="headerlink" title="8. 方法调用和返回指令"></a>8. 方法调用和返回指令</h2><p>由下面 5 中指令来调用方法：</p><ul><li><code>invokevirtual</code>：调用对象实例的一个指定方法，这是 Java 语言中常见的方法调用；</li><li><code>invokeinterface</code>：调用接口方法，搜索运行时实现了该接口方法的对象，并调用；</li><li><code>invokespecial</code>：调用特殊方法，比如实例化方法 <code>&lt;init&gt;</code> 和 <code>&lt;clinit&gt;</code>、私有方法或超类中的方法；</li><li><code>invokestatic</code>：调用类的静态方法；</li><li><code>invokedynamic</code>：调用 <code>Lambda</code> 动态绑定方法。<br>方法的返回指令分别为：<code>ireturn</code> <code>lreturn</code> <code>freturn</code> <code>dreturn</code> <code>areturn</code> <code>return</code>。</li></ul><h2 id="9-抛出异常"><a href="#9-抛出异常" class="headerlink" title="9. 抛出异常"></a>9. 抛出异常</h2><p><code>athrow</code></p><h2 id="10-同步-指令"><a href="#10-同步-指令" class="headerlink" title="10. 同步 指令"></a>10. 同步 指令</h2><p>使用 <code>synchronized</code> 修饰的语句块：<code>monitorenter</code> <code>monitorexit</code></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Java 内存模型基础</title>
      <link>https://yangshifu.xyz/posts/jmm-basic/</link>
      <description>
        <![CDATA[<p>在多核的计算机系统中，处理器通常拥有一个或多个处理器高速缓存（如 L1、L2 和 L3 等），用来提高数据的读取速度（因为 L1、L2 和 L3 等高速缓存从物理级别更接近处理器）和减少共享数据在总线上的传输（因为大部分的内存操作基本都能在 CPU 的本地高速缓存完成）。高]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/java/">java</category>
      <pubDate>Fri, 21 Aug 2026 19:03:51 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在多核的计算机系统中，处理器通常拥有一个或多个处理器高速缓存（如 L1、L2 和 L3 等），用来提高数据的读取速度（因为 L1、L2 和 L3 等高速缓存从物理级别更接近处理器）和减少共享数据在总线上的传输（因为大部分的内存操作基本都能在 CPU 的本地高速缓存完成）。高速缓存可以极大地提高性能，但是也带来了新的挑战。例如：两个处理器同时访问一个内存地址，在何种条件下他们都能看到内存中同样的值？</p><p>在处理器层面，内存模型定义了<strong>其他处理器的写入对于当前处理器可见，及当前处理器的写入对其他处理器可见</strong>的充要条件。一些处理器使用的是强内存模型，任何时间任意处理器所看到的任意内存地址的值都是一致的。还有一些处理器使用了较弱的内存模型，主要通过一些特殊的指令，也称为内存屏障，来实现将本地内存中的数据刷到主存或让这些数据无效（标记数据无效会使得处理器必须从主存读取该数据），使得其中一个处理器的写操作对于其他处理器都是可见的。这些内存屏障通常在执行锁定和解锁操作时执行，对于高级语言的程序员来说，它们是不可见的。</p><p>因为减少了对内存屏障的依赖，在强内存模型下更容易编写程序。但即使是在最强的内存模型下，内存屏障也是必须的；近年来，处理器的设计趋势更倾向于较弱的内存模型，因为更松散的内存一致性要求更有利于大规模的多处理器、高内存的系统扩展。</p><p>由于编译器对代码重排序，一个写操作能否对其他线程可见成为了问题。在不影响代码语义的情况下，编译器就会将代码重排序。由于缓存的影响，如果编译器重排序时将一个写操作滞后，那么其他线程将只能在这个写操作完成后才能看到其结果。</p><p>此外，对内存的写也可以排在前面，在这种情况下，其他线程将会在代码实际执行之前就看到其结果，这里的实际执行指的就是源码中的顺序，而实际执行的顺序是重排序后的顺序。所有的这些灵活性的设计，都是为了给编译器、运行时或者硬件在最佳的顺序下灵活地执行操作，在内存模型的限制之下，我们能实现更高的性能。</p><h2 id="java-内存模型（java-memory-model-jmm"><a href="#java-内存模型（java-memory-model-jmm" class="headerlink" title="Java 内存模型（Java Memory Model, JMM)"></a>Java 内存模型（Java Memory Model, JMM)</h2><p>JMM 描述了什么样的行为在多线程代码中是合法的，以及线程如何通过内存进行交互；描述了程序中变量之间的关系；也描述了在底层中数据从内存或寄存器的存取细节。在多种硬件及多种形式的编译器优化下依然能够保持程序的正确性。</p><p>Java 包括几种语言结构：<code>volatile</code>，<code>final</code> 和 <code>synchronized</code>，用以帮助程序开发者正确地为编译器描述程序的并发依赖。JMM 定义了 <code>volatile</code> 和 <code>synchronized</code> 的行为，更重要的是，其保证了一个正确的同步程序在多处理器架构上也能正确运行。</p><h2 id="原则"><a href="#原则" class="headerlink" title="原则"></a>原则</h2><h3 id="原子性"><a href="#原子性" class="headerlink" title="原子性"></a>原子性</h3><p>在一个线程中的一系列操作，其中间过程不会被外部知晓，外部只能看到最终结果，而无法对其中间过程进行干扰。</p><h3 id="可见性"><a href="#可见性" class="headerlink" title="可见性"></a>可见性</h3><p>在一个线程中对共享变量对修改，在另外一个线程中能够立即看到。</p><h3 id="有序性"><a href="#有序性" class="headerlink" title="有序性"></a>有序性</h3><p>编译优化、处理器执行优化均不能改变语句的语义。</p><h2 id="同步"><a href="#同步" class="headerlink" title="同步"></a>同步</h2><p>线程之间的通信机制有两种：共享内存和消息传递。</p><p>Java 采用的是共享内存模型。</p><h2 id="重排序"><a href="#重排序" class="headerlink" title="重排序"></a>重排序</h2><h3 id="1-编译器优化的重排序"><a href="#1-编译器优化的重排序" class="headerlink" title="1. 编译器优化的重排序"></a>1. 编译器优化的重排序</h3><p>编译器在不改变单线程程序语义的前提下，可以重新安排语句的执行顺序。</p><h3 id="2-指令级并行重排序"><a href="#2-指令级并行重排序" class="headerlink" title="2. 指令级并行重排序"></a>2. 指令级并行重排序</h3><p>现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性，处理器可以改变语句对应机器指令的执行顺序。</p><h3 id="3-内存系统的重排序"><a href="#3-内存系统的重排序" class="headerlink" title="3. 内存系统的重排序"></a>3. 内存系统的重排序</h3><p>由于处理器使用缓存和读写缓冲区，这使得加载和存储操作看上去像是在乱序执行。</p><p>为了保证内存可见性，Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。</p><h2 id="内存屏障"><a href="#内存屏障" class="headerlink" title="内存屏障"></a>内存屏障</h2><p>JMM 把内存屏障指令分为 4 类：</p><h3 id="1-loadload-barriers"><a href="#1-loadload-barriers" class="headerlink" title="1. LoadLoad Barriers"></a>1. LoadLoad Barriers</h3><p><strong>Load1;LoadLoad;Load2</strong> 确保 Load1 数据的装载先于 Load2 及所有后续装载指令的装载；</p><h3 id="2-storestore-barriers"><a href="#2-storestore-barriers" class="headerlink" title="2. StoreStore Barriers"></a>2. StoreStore Barriers</h3><p><strong>Store1;StoreStore;Store2</strong> 确保 Store1 数据对其他处理器可见（刷新到内存）先于 Store2 及所有后续存储指令对存储；</p><h3 id="3-loadstore-barriers"><a href="#3-loadstore-barriers" class="headerlink" title="3. LoadStore Barriers"></a>3. LoadStore Barriers</h3><p><strong>Load1;LoadStore;Store2</strong> 确保 Load1 数据装载先于 Store2 及所有后续的存储指令刷新到内存；</p><h3 id="4-storeload-barriers"><a href="#4-storeload-barriers" class="headerlink" title="4. StoreLoad Barriers"></a>4. StoreLoad Barriers</h3><p><strong>Store1;StoreLoad;Load2</strong> 确保 Store1 数据对其他处理器变得可见（刷新到内存）先于 Load2 及所有后续装载指令的装载。该屏障会使在其之前的所有内存访问指令（存储和装载指令）完成之后，才执行该屏障之后的内存访问指令。</p><p>其中，<strong>StoreLoad Barriers</strong> 是一个全能型屏障，它同时具有其他 3 个屏障的效果。而执行该屏障开销会很昂贵，因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中（Buffer Fully Flush）。</p><h2 id="happens-before"><a href="#happens-before" class="headerlink" title="Happens-Before"></a>Happens-Before</h2><p>Happens-Before 指的是：<strong>前一个操作的结果对后续操作可见</strong>。</p><p>与程序员密切相关的 7 条规则如下：</p><ol><li>程序顺序规则：一个线程中的每个操作，均 Happens-Before 于该线程中的任意后续操作；</li><li>监视器锁规则：Monitor Lock，通常是 <code>synchronized</code> 关键字，对于一个锁的解锁，均 Happens-Before 于随后对这个锁对加锁；</li><li>Volatile 变量规则：对于一个 <code>volatile</code> 变量对写操作，Happens-Before 于任意后续对这个 <code>volatile</code> 变量的读；</li><li>线程启动规则：Thread 对象的 <code>Thread#start()</code> 方法 Happens-Before 于此线程的每一个动作；</li><li>线程终止规则：线程中的所有操作均 Happens-Before 于该线程都终止；</li><li>线程中断规则：对线程的 <code>Thread#interrupt()</code> 方法的调用 Happens-Before 于该线程中的代码检测到中断事件的发生；</li><li>对象终结规则：一个对象的初始化完成（构造函数执行结束）Happens-Before 于它的 <code>Object#finalize()</code> 方法的开始。</li></ol><p>Happens-Before 的特性传递性：</p><p>如果 A Happens-Before B，且 B Happens-Before C，那么 A Happens-Before C。</p><h2 id="数据竞争"><a href="#数据竞争" class="headerlink" title="数据竞争"></a>数据竞争</h2><p>JMM 规范对数据竞争对定义如下：</p><ol><li>在一个线程中写一个变量；</li><li>在另一个线程中读同一个变量；</li><li>读和写都没有通过同步来排序。</li></ol><p>一个有数据竞争的程序就是<strong>没有正确同步的有问题的</strong>程序。</p><h2 id="顺序一致性"><a href="#顺序一致性" class="headerlink" title="顺序一致性"></a>顺序一致性</h2><p>顺序一致性内存模型有两大特性：</p><ol><li>一个线程中都所有操作必须按照程序都顺序来执行；</li><li>所有线程都只能看到一个单一的操作执行顺序，且线程中的每个操作都必须原子执行且立刻对所有线程可见。</li></ol><p>假设有两个线程 A 和 B 并发执行。A 线程中有三个操作，它们的顺序为： A1-A2-A3；B 线程中也有三个操作，它们的顺序为：B1-B2-B3。</p><ul><li>假设两个线程使用锁来进行正确的同步，如 synchronized 关键字或显式声明的锁等，那么两个线程的执行顺序为：</li></ul><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">A1-&gt;A2-&gt;A3-&gt;B1-&gt;B2-&gt;B3<br></code></pre></td></tr></table></figure><ul><li>假设两个线程没有进行同步，那么它们的执行顺序可能为：</li></ul><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">B1-&gt;A1-&gt;B2-&gt;A2-&gt;A3-&gt;B3<br></code></pre></td></tr></table></figure><p>虽然它们的执行顺序是无序的，但两个线程都只能看到一个一致的整体执行顺序，即在两个线程中，都是如上所示的顺序。</p><p>但以上这个情况在 JMM 没有得到保证，在 JMM 中，两个线程整体的执行顺序是无序的，而且两个线程看到的操作执行顺序也可能不一致。比如，在 A 线程中将写过的数据缓存在本地内存中，在没有刷新到主存之前，从 B 线程中观察，会认为这个写操作根本没有执行。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Java 代码编译</title>
      <link>https://yangshifu.xyz/posts/java-code-compilation/</link>
      <description>本文将简单讨论 Java 源代码如何编译为 JVM 指令</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/java/">java</category>
      <pubDate>Fri, 21 Aug 2026 18:57:20 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>“编译器”一词，有时也指将 JVM 指令集翻译为特定 CPU 架构指令集的工具，比如 JIT（just-in-time）即时编译器。但本文档讨论的范围仅包括将 Java 源代码编译为 JVM 指令。</p><span id="more"></span><h3 id="1-格式示例"><a href="#1-格式示例" class="headerlink" title="1. 格式示例"></a>1. 格式示例</h3><p><code>javap</code> 工具用来反编译 Java 字节码，反编译后的指令示例如下：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">&lt;index&gt; &lt;opcode&gt; [ &lt;operand1&gt; [&lt;operand2&gt;...] ] [&lt;comment&gt;]<br></code></pre></td></tr></table></figure><p><code>&lt;index&gt;</code> 是当前方法内指令数组所对应的下标，可以理解为从方法开始处以字节为单位的偏移。<code>&lt;opcode&gt;</code> 是指令操作码的助记符。0 个或多个 是指令的操作数。<code>&lt;comment&gt;</code> 则是可选的注释语法。比如以下示例：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">8    bipush    100    // Push int constant 100<br></code></pre></td></tr></table></figure><p>通常注释是由 <code>javap</code> 命令产生的。每个指令的前缀 <code>&lt;index&gt;</code> 可用作控制传输指令的目标，比如 <code>goto 8</code> 指令将控制权转移到所以为 8 的指令处。JVM 在实际进行控制传输时使用的操作数其实是相对于该指令的偏移地址，在这里 <code>javap</code> 将其显示地直观。</p><p>通常使用 <code>#</code> 来指定运行时常量池的索引，比如：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">10    ldc    #1    // Push float constant 100.0<br></code></pre></td></tr></table></figure><p>或者</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">9    invokevirtual    #4    // Method Example.addTwo(II)I<br></code></pre></td></tr></table></figure><h3 id="2-常量、局部变量表及控制流程的使用"><a href="#2-常量、局部变量表及控制流程的使用" class="headerlink" title="2. 常量、局部变量表及控制流程的使用"></a>2. 常量、局部变量表及控制流程的使用</h3><p>下面的 <code>spin</code> 方法简单地空循环100次：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-type">void</span> <span class="hljs-title function_">spin</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-type">int</span> i;<br>    <span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) &#123;<br>        ;    <span class="hljs-comment">// Loop body is empty</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>编译器将代码编译为：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">0   iconst_0       // Push int constant 0<br>1   istore_1       // Store into local variable 1 (i=0)<br>2   goto 8         // First time through don&#x27;t increment<br>5   iinc 1 1       // Increment local variable 1 by 1 (i++)<br>8   iload_1        // Push local variable 1 (i)<br>9   bipush 100     // Push int constant 100<br>11  if_icmplt 5    // Compare and loop if less than (i &lt; 100)<br>14  return         // Return void when done<br></code></pre></td></tr></table></figure><p>JVM 是以栈（stack-oriented）为主导的，许多指令将所需的操作数从操作数栈取出，执行指令，然后将结果压回操作数栈中。每个方法执行时都创建一个栈帧，并在其中创建操作数栈和局部变量，以供方法使用。在程序执行的过程中，每个线程需要管理许多的栈帧和许多的操作数栈来与嵌套的方法调用对应。每个栈帧中同一时刻只会有一个操作数栈处于激活状态。</p><p>在 <code>spin</code> 方法中，有两个常量：<code>0</code> 和 <code>100</code>，在加载到操作数栈时使用了不同的指令，<code>0</code> 使用的是 <code>iconst_0</code> 指令，而 <code>100</code> 使用的是 <code>bipush</code> 指令，此指令将使得 <code>100</code> 成为立即操作数。</p><blockquote><p>对于 <code>int</code> 值的不同，使用的指令也不同：取值 -1<del>5 采用 iconst 指令，取值 -128</del>127 采用 bipush 指令，取值 -32768<del>32767 采用 sipush 指令，取值 -2147483648</del>2147483647 采用 ldc 指令。</p></blockquote><p><code>int i</code> 保存在局部变量表中的第一个，也就是索引 1 的位置。因为 JVM 指令在执行时总是使用操作数栈中的值，而不是直接使用局部变量，所以值在局部变量表和操作数栈中的相互传递在编译后的字节码中很常见。在 <code>spin</code> 方法中，变量 <code>i</code> 的相互传递是通过 <code>istore_1</code> 和 <code>iload_1</code> 来完成的，它们都是对第 <code>1</code> 个局部变量进行操作：<code>istore_1</code> 的作用是将一个 <code>int</code> 值从操作数栈中弹出并保存在局部变量表索引 1 的位置，而 <code>iload_1</code> 则是将局部变量表索引 1 位置的值压入操作数栈中。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Java 字节码结构</title>
      <link>https://yangshifu.xyz/posts/java-bytecode-structure/</link>
      <description>
        <![CDATA[<h2 id="字节码结构"><a href="#字节码结构" class="headerlink" title="字节码结构"></a>字节码结构</h2><table>
<thead>
<tr>
<th align="center">区域</th>
<th align="ce]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/java/">java</category>
      <pubDate>Fri, 21 Aug 2026 18:47:30 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="字节码结构"><a href="#字节码结构" class="headerlink" title="字节码结构"></a>字节码结构</h2><table><thead><tr><th align="center">区域</th><th align="center">大小（字节）</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">魔数</td><td align="center">4</td><td align="center">固定为 0xCAFEBABE</td></tr><tr><td align="center">副版本号</td><td align="center">2</td><td align="center">Minor Version</td></tr><tr><td align="center">主版本号</td><td align="center">2</td><td align="center">Majar Version</td></tr><tr><td align="center">常量池计数器</td><td align="center">2</td><td align="center">常量池大小</td></tr><tr><td align="center">常量池数据区</td><td align="center"></td><td align="center">字面量和符号引用</td></tr><tr><td align="center">访问标志</td><td align="center">2</td><td align="center">表示访问权限</td></tr><tr><td align="center">类索引</td><td align="center">2</td><td align="center">确定当前类的全限定名</td></tr><tr><td align="center">超类索引</td><td align="center">2</td><td align="center">确定当前类的超类全限定名</td></tr><tr><td align="center">接口计数器</td><td align="center">2</td><td align="center">接口数量</td></tr><tr><td align="center">接口表</td><td align="center"></td><td align="center">接口索引数组集合</td></tr><tr><td align="center">字段计数器</td><td align="center">2</td><td align="center">字段数量</td></tr><tr><td align="center">字段表</td><td align="center"></td><td align="center">字段数组</td></tr><tr><td align="center">方法计数器</td><td align="center">2</td><td align="center">当前类的方法数量</td></tr><tr><td align="center">方法表</td><td align="center"></td><td align="center">方法数组集合</td></tr><tr><td align="center">属性计数器</td><td align="center">2</td><td align="center">属性数量</td></tr><tr><td align="center">属性表</td><td align="center"></td><td align="center">属性数组集合</td></tr></tbody></table><h2 id="魔数"><a href="#魔数" class="headerlink" title="魔数"></a>魔数</h2><p>一个有效的 Java 字节码文件的前 4 个字节固定为：<code>0XCAFEBABE</code>，其他同样使用魔数的文件类型为：<code>jpeg</code>、<code>png</code>、<code>gif</code> 等。</p><p>JVM 用魔数来校验文件是否是一个有效且合法的字节码文件。</p><h2 id="版本号"><a href="#版本号" class="headerlink" title="版本号"></a>版本号</h2><p>主版本号（M）和次版本号（m）限定了字节码文件支持的 JVM 版本。</p><h2 id="常量池计数器和常量池"><a href="#常量池计数器和常量池" class="headerlink" title="常量池计数器和常量池"></a>常量池计数器和常量池</h2><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>常量池中的有效数据是从下标 1 开始的，0 下标保留，则常量池计数器也是从 1 开始的。</p><p>常量池中主要用于存放字面量和符号引用，其中：</p><p>字面量由文字字符串、final 常量值等构成；<br>符号引用则包括了类和接口的全限定名（Fully Qualified Name，FQN）、字段的名称和描述符（Descriptor），以及方法的名称和描述符。</p><p>假设有如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// Test.java</span><br><span class="hljs-keyword">package</span> com.example;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Test</span> &#123;<br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-type">double</span> <span class="hljs-variable">j</span> <span class="hljs-operator">=</span> <span class="hljs-number">1.23</span>;<br>  <span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;<br>  <span class="hljs-keyword">private</span> <span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;HelloWorld!&quot;</span>;<br>  <span class="hljs-keyword">public</span> <span class="hljs-type">void</span> <span class="hljs-title function_">hello</span><span class="hljs-params">()</span> &#123;<br>    System.out.println(s);<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>首先将其编译为 Java 字节码：<code>javac Test.java</code>，然后使用 javap 工具查看其字节码内容：<code>javap -v Test</code>，截取 <code>Constant pool</code> 段如下：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext"> #1 = Methodref          #11.#26        // java/lang/Object.&quot;&lt;init&gt;&quot;:()V<br> #2 = Double             1.23d<br> #4 = Fieldref           #10.#27        // com/example/Test.j:D<br> #5 = Fieldref           #10.#28        // com/example/Test.i:I<br> #6 = String             #29            // HelloWorld!<br> #7 = Fieldref           #10.#30        // com/example/Test.s:Ljava/lang/String;<br> #8 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;<br> #9 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V<br>#10 = Class              #35            // com/example/Test<br>#11 = Class              #36            // java/lang/Object<br>#12 = Utf8               j<br>#13 = Utf8               D<br>#14 = Utf8               ConstantValue<br>#15 = Utf8               i<br>#16 = Utf8               I<br>#17 = Utf8               s<br>#18 = Utf8               Ljava/lang/String;<br>#19 = Utf8               &lt;init&gt;<br>#20 = Utf8               ()V<br>#21 = Utf8               Code<br>#22 = Utf8               LineNumberTable<br>#23 = Utf8               hello<br>#24 = Utf8               SourceFile<br>#25 = Utf8               Test.java<br>#26 = NameAndType        #19:#20        // &quot;&lt;init&gt;&quot;:()V<br>#27 = NameAndType        #12:#13        // j:D<br>#28 = NameAndType        #15:#16        // i:I<br>#29 = Utf8               HelloWorld!<br>#30 = NameAndType        #17:#18        // s:Ljava/lang/String;<br>#31 = Class              #37            // java/lang/System<br>#32 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;<br>#33 = Class              #40            // java/io/PrintStream<br>#34 = NameAndType        #41:#42        // println:(Ljava/lang/String;)V<br>#35 = Utf8               com/example/Test<br>#36 = Utf8               java/lang/Object<br>#37 = Utf8               java/lang/System<br>#38 = Utf8               out<br>#39 = Utf8               Ljava/io/PrintStream;<br>#40 = Utf8               java/io/PrintStream<br>#41 = Utf8               println<br>#42 = Utf8               (Ljava/lang/String;)V<br></code></pre></td></tr></table></figure><p>第 #1 行表示存储的是一个方法引用，指向了 #11 和 #26，并用<code>.</code>连接它们；</p><p>找到 #11，它是一个类的符号引用，指向了 #36；找到 #26 ，它是一个字段或方法的符号引用，指向了 #19 和 #20，用<code>:</code>连接；</p><p>找到 #36，它是一个 UTF-8 编码的字符串：<code>java/lang/Object</code>；找到 #19 和 #20，它们都是 UTF-8 编码的字符串<code>&lt;init&gt;:()V</code>；</p><p>最终得到 #1 行的方法引用指向了<code>java/lang/Object.&quot;&lt;init&gt;&quot;:()V</code>，其他以此类推。</p><h3 id="常量池中的表类型："><a href="#常量池中的表类型：" class="headerlink" title="常量池中的表类型："></a>常量池中的表类型：</h3><table><thead><tr><th align="center">javap 结果</th><th align="center">类型</th></tr></thead><tbody><tr><td align="center">Utf8</td><td align="center">UTF-8 编码的字符串</td></tr><tr><td align="center">Integer</td><td align="center">整形字面值</td></tr><tr><td align="center">Float</td><td align="center">单精度浮点数字面值</td></tr><tr><td align="center">Long</td><td align="center">长整型字面值</td></tr><tr><td align="center">Double</td><td align="center">双精度副店数字面值</td></tr><tr><td align="center">Class</td><td align="center">类或接口的符号引用</td></tr><tr><td align="center">String</td><td align="center">字符串类型的字面值</td></tr><tr><td align="center">Fieldref</td><td align="center">字段的符号引用</td></tr><tr><td align="center">Methodref</td><td align="center">方法的符号引用</td></tr><tr><td align="center">InterfaceMethodref</td><td align="center">接口中方法的符号引用</td></tr><tr><td align="center">NameAndType</td><td align="center">字段或方法的部分符号引用</td></tr><tr><td align="center">MethodHandle</td><td align="center">方法句柄</td></tr><tr><td align="center">MethodType</td><td align="center">方法类型</td></tr><tr><td align="center">InvokeDynamic</td><td align="center">动态方法调用点</td></tr></tbody></table><h2 id="访问标志"><a href="#访问标志" class="headerlink" title="访问标志"></a>访问标志</h2><p>常量池之后的两个字节存储的就是访问标志（access flags），主要用于表示该类或接口的访问权限。</p><p>有如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// Public.java</span><br><span class="hljs-keyword">package</span> com.example;<br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Public</span> &#123;&#125;<br></code></pre></td></tr></table></figure><p>编译为字节码后用 <code>javap -v Public</code> 查看，截取访问标志段如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java">flags: ACC_PUBLIC, ACC_SUPER<br></code></pre></td></tr></table></figure><p>代表该类声明为了 <code>public</code>，可被包外的类访问。</p><table><thead><tr><th align="center">标志名称</th><th align="center">值</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">ACC_PUBLIC</td><td align="center">0x0001</td><td align="center">声明为 public，可从包外访问</td></tr><tr><td align="center">ACC_FINAL</td><td align="center">0x0010</td><td align="center">声明为 final，不允许派生</td></tr><tr><td align="center">ACC_SUPER</td><td align="center">0x0020</td><td align="center">当用到 invokespecial 指令时，需要特殊处理的超类方法</td></tr><tr><td align="center">ACC_INTERFACE</td><td align="center">0x0200</td><td align="center">标志定义的是接口而不是类</td></tr><tr><td align="center">ACC_ABSTRACT</td><td align="center">0x0400</td><td align="center">声明为 abstract，不能被实例化</td></tr><tr><td align="center">ACC_SYNTHETIC</td><td align="center">0x1000</td><td align="center">声明为 synthetic，标识并非由 Java 代码生成</td></tr><tr><td align="center">ACC_ANNOTATION</td><td align="center">0x2000</td><td align="center">标识注解类型</td></tr><tr><td align="center">ACC_ENUM</td><td align="center">0x4000</td><td align="center">标识枚举类型</td></tr></tbody></table><h2 id="类索引"><a href="#类索引" class="headerlink" title="类索引"></a>类索引</h2><p>当前类索引和超类索引都是指向常量池中的 Class 类型，Class 类型又指向 Utf8 类型，最终确定类的全限定名。</p><p>Java 语法规范规定只允许继承一个类，如果当前类没有显式继承任何超类时，则它的直接超类就是 Object，父类索引为 0，即指向了常量池中的下标 0 保留项。</p><h2 id="接口计数器和接口表"><a href="#接口计数器和接口表" class="headerlink" title="接口计数器和接口表"></a>接口计数器和接口表</h2><p>接口计数器用于表示当前类或者接口的直接超类接口数量；接口表实际是一个数组集合，它包含了当前类或接口的直接超类在常量池中的索引集合，通过这个索引即可确定当前类或接口的超类接口全限定名。</p><h2 id="字段计数器和字段表"><a href="#字段计数器和字段表" class="headerlink" title="字段计数器和字段表"></a>字段计数器和字段表</h2><p>字段计数器标识一个字节码文件中的字段总数，包括类变量和实例变量；字段表则是一个数组集合，其中的每个成员都表示了一个字段的完整信息。</p><p>有如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// Types.java</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Types</span> &#123;<br><br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;HelloWorld!&quot;</span>;<br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-type">double</span> <span class="hljs-variable">pi</span> <span class="hljs-operator">=</span> <span class="hljs-number">3.14</span>;<br>  <span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">123</span>;<br>  protect <span class="hljs-type">Integer</span> <span class="hljs-variable">j</span> <span class="hljs-operator">=</span> <span class="hljs-number">456</span>;<br>  <span class="hljs-keyword">private</span> <span class="hljs-type">Integer</span>[] <span class="hljs-variable">arr</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Integer</span>[]&#123;<span class="hljs-number">7</span>, <span class="hljs-number">8</span>, <span class="hljs-number">9</span>&#125;;<br>&#125;<br></code></pre></td></tr></table></figure><p>编译为字节码后通过 <code>javap</code> 查看：<code>javap -p -v Types</code>，截取字段部分如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> java.lang.String s;<br>  descriptor: Ljava/lang/String;<br>  flags: ACC_PUBLIC, ACC_STATIC<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-type">double</span> pi;<br>  descriptor: D<br>  flags: ACC_PUBLIC, ACC_FINAL<br>  ConstantValue: <span class="hljs-type">double</span> <span class="hljs-number">3.14d</span><br><br><span class="hljs-keyword">public</span> <span class="hljs-type">int</span> i;<br>  descriptor: I<br>  flags: ACC_PUBLIC<br><br><span class="hljs-keyword">protected</span> java.lang.Integer j;<br>  descriptor: Ljava/lang/Integer;<br>  flags: ACC_PROTECTED<br><br><span class="hljs-keyword">private</span> java.lang.Integer[] arr;<br>  descriptor: [Ljava/lang/Integer;<br>  flags: ACC_PRIVATE<br></code></pre></td></tr></table></figure><p>其中每项都完整描述了该字段的相关信息，如描述符（descriptor）、访问标志（flags），甚至被声明为 final 的字段还携带了常量值（ConstantValue）。</p><p>字段描述符总结如下：</p><table><thead><tr><th align="center">Descriptor</th><th align="center">类型</th><th align="center">含义</th></tr></thead><tbody><tr><td align="center">B</td><td align="center">byte</td><td align="center">字节</td></tr><tr><td align="center">C</td><td align="center">char</td><td align="center">字符</td></tr><tr><td align="center">D</td><td align="center">double</td><td align="center">双精度浮点</td></tr><tr><td align="center">F</td><td align="center">float</td><td align="center">单精度浮点</td></tr><tr><td align="center">I</td><td align="center">int</td><td align="center">整形</td></tr><tr><td align="center">J</td><td align="center">long</td><td align="center">长整型</td></tr><tr><td align="center">S</td><td align="center">short</td><td align="center">短整型</td></tr><tr><td align="center">Z</td><td align="center">boolean</td><td align="center">布尔</td></tr><tr><td align="center">L</td><td align="center">reference</td><td align="center">对象类型</td></tr><tr><td align="center">[</td><td align="center">reference</td><td align="center">数组类型</td></tr></tbody></table><p>有如上总结表后，可得知：</p><p>上例中的 j，描述符为 <code>Ljava/lang/Integer</code>，即代表 <code>Integer</code> 对象；arr 描述符为 <code>[Ljava/lang/Integer</code>，即代表 <code>Integer</code> 数组。</p><h2 id="方法计数器和方法表"><a href="#方法计数器和方法表" class="headerlink" title="方法计数器和方法表"></a>方法计数器和方法表</h2><p>方法计数器代表该类中的方法总数，仅包含该类或接口定义的方法，不包含继承而来的方法。</p><p>方法表则是一个数组集合，其中每项都表示了一个方法的完整信息。</p><p>有如下代码：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// Methods.java</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Methods</span> &#123;<br>  <span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">intPublic</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>  &#125;<br><br>  <span class="hljs-keyword">private</span> <span class="hljs-type">String</span> <span class="hljs-title function_">strPrivate</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;HelloWorld!&quot;</span>;<br>  &#125;<br><br>  <span class="hljs-keyword">protected</span> <span class="hljs-type">Object</span> <span class="hljs-title function_">objectProtected</span><span class="hljs-params">()</span> &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>();<br>  &#125;<br><br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">void</span> <span class="hljs-title function_">voidStatic</span><span class="hljs-params">()</span> &#123;<br><br>  &#125;<br><br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-type">void</span> <span class="hljs-title function_">finalPublic</span><span class="hljs-params">()</span> &#123;<br><br>  &#125;<br><br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-type">void</span> <span class="hljs-title function_">syncPublic</span><span class="hljs-params">()</span> &#123;<br><br>  &#125;<br><br>  <span class="hljs-keyword">private</span> <span class="hljs-type">void</span> <span class="hljs-title function_">dynamicArgs</span><span class="hljs-params">(String first, Object... args)</span> &#123;<br><br>  &#125;<br><br>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-type">void</span> <span class="hljs-title function_">abstractPublic</span><span class="hljs-params">()</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>编译为字节码后使用 javap 查看，并截取方法定义部分（仅保留描述符和访问标志）如下：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-type">public</span> <span class="hljs-title function_">Methods</span><span class="hljs-params">()</span>;<br>  descriptor: ()V<br>  flags: ACC_PUBLIC<br><br><span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">intPublic</span><span class="hljs-params">()</span>;<br>  descriptor: ()I<br>  flags: ACC_PUBLIC<br><br><span class="hljs-keyword">private</span> java.lang.<span class="hljs-type">String</span> <span class="hljs-title function_">strPrivate</span><span class="hljs-params">()</span>;<br>  descriptor: ()Ljava/lang/String;<br>  flags: ACC_PRIVATE<br><br><span class="hljs-keyword">protected</span> java.lang.<span class="hljs-type">Object</span> <span class="hljs-title function_">objectProtected</span><span class="hljs-params">()</span>;<br>  descriptor: ()Ljava/lang/Object;<br>  flags: ACC_PROTECTED<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">void</span> <span class="hljs-title function_">voidStatic</span><span class="hljs-params">()</span>;<br>  descriptor: ()V<br>  flags: ACC_PUBLIC, ACC_STATIC<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-type">void</span> <span class="hljs-title function_">finalPublic</span><span class="hljs-params">()</span>;<br>  descriptor: ()V<br>  flags: ACC_PUBLIC, ACC_FINAL<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-type">void</span> <span class="hljs-title function_">syncPublic</span><span class="hljs-params">()</span>;<br>  descriptor: ()V<br>  flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br><br><span class="hljs-keyword">private</span> <span class="hljs-type">void</span> <span class="hljs-title function_">dynamicArgs</span><span class="hljs-params">(java.lang.String, java.lang.Object...)</span>;<br>  descriptor: (Ljava/lang/String;[Ljava/lang/Object;)V<br>  flags: ACC_PRIVATE, ACC_VARARGS<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-type">void</span> <span class="hljs-title function_">abstractPublic</span><span class="hljs-params">()</span>;<br>  descriptor: ()V<br>  flags: ACC_PUBLIC, ACC_ABSTRACT<br></code></pre></td></tr></table></figure><p>方法访问标志信息如下表：</p><table><thead><tr><th align="center">标志名称</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">ACC_PUBLIC</td><td align="center">声明方法为 public</td></tr><tr><td align="center">ACC_PRIVATE</td><td align="center">声明方法为 private</td></tr><tr><td align="center">ACC_PROTECTED</td><td align="center">声明方法为 protected</td></tr><tr><td align="center">ACC_STATIC</td><td align="center">声明方法为类方法</td></tr><tr><td align="center">ACC_FINAL</td><td align="center">声明方法为 final</td></tr><tr><td align="center">ACC_SYNCHRONIZED</td><td align="center">声明方法为 synchronized</td></tr><tr><td align="center">ACC_VARARGS</td><td align="center">声明方法有变长参数</td></tr><tr><td align="center">ACC_ABSTRACT</td><td align="center">声明方法为 abstract</td></tr></tbody></table>]]>
      </content:encoded>
    </item>
    <item>
      <title>Java HashMap</title>
      <link>https://yangshifu.xyz/posts/java-hashmap/</link>
      <description>
        <![CDATA[<h2 id="基本描述"><a href="#基本描述" class="headerlink" title="基本描述"></a>基本描述</h2><p><code>HashMap</code> 是一个基于哈希表(Hash table)的 <code>Map</code> 接口]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <pubDate>Fri, 21 Aug 2026 18:39:05 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="基本描述"><a href="#基本描述" class="headerlink" title="基本描述"></a>基本描述</h2><p><code>HashMap</code> 是一个基于哈希表(Hash table)的 <code>Map</code> 接口的实现，它实现了 <code>Map</code> 接口的所有操作，并允许空值和空键。</p><p>它除了非线程安全和允许空值空键外，大致和 <code>Hashtable</code> 是一致的；并且它也不能保证其中元素的顺序，实际上，随着时间的推移，其中元素的顺序也会发生改变。</p><p>在元素没有冲突、所有元素都能够均匀地分散到其中的各个 <code>bucket</code> 的情况下，<code>HashMap</code> 基本的 <code>get</code> 和 <code>put</code> 操作的时间复杂度是常量的，即 O(n)。</p><p>对 <code>HashMap</code> 实例迭代的时间与它的容量 (<code>bucket</code> 的数量) 加上大小 (<code>bucket</code> 的大小) 成正比。因此，如果需要较好的迭代性能，就不要将初始的容量 (<code>capaticy</code>) 设置得过高，或者负载系数 (<code>load factor</code>) 设置得过低。</p><p><code>HashMap</code> 实例的两个参数将直接影响它的性能，初始化容量 (<code>capacity</code>) 和负载系数 (<code>load factor</code>)。容量指的是 <code>HashMap</code> 内部桶 (<code>bucket</code>) 的个数，而负载系数 (<code>load factor) 则决定了</code>HashMap<code>进行扩容的临界值。如果</code>HashMap<code>中元素的实际个数</code>size<code>超过了容量和负载系数的乘积，那么</code>HashMap<code>就会重新进行散列 (</code>rehashed&#96;)，内部的数据结构将被重建为接近原来的两倍。</p><p>按照惯例，负载系数的默认值 <code>0.75</code> 在时间和空间的开销上取得了最佳权衡。更高的值减少了空间的浪费，却提高了查表的开销，影响了大部分的 <code>HashMap</code> 操作，包括 <code>get</code> 和 <code>put</code>。在设置 <code>HashMap</code> 的初始容量时，应考虑预期的元素个数和负载系数，为其设置合适的初始值，减少在使用过程中重新进行散列 (<code>rehashed</code>) 的操作。如果元素的最大个数始终小于初始容量与负载系数的乘积，则不会重新进行散列。</p><p>如果 <code>HashMap</code> 中要存储很多元素，那么在创建 <code>HashMap</code> 实例时就应该使用足够大的初始化容量，这样的情况下存储将更加有效，因为不需要进行重新散列(<code>rehash</code>)来扩展。当 <code>HashMap</code> 中的键 <code>Key</code> 的 <code>hashCode()</code> 方法返回值相同时，将严重降低 <code>HashMap</code> 的性能，所以我们可以重写键类的 <code>hashCode()</code> 方法来进行优化，或者实现 <code>Comparable</code> 接口的 <code>compareTo()</code> 方法。</p><p><code>HashMap</code> 不是线程安全的，如果多个线程同时进行读写，那么需要在外部保证其线程安全。这里的『写』包括添加和删除元素，不包括替换已有的元素。通常通过封装对 <code>HashMap</code> 的操作来实现同步（如：在对 <code>HashMap</code> 进行操作的方法加上 <code>synchronized</code> 或者使用别的同步手段）。</p><p>如果没有进行封装的对象，也可以在创建 <code>HashMap</code> 时使用 <code>Collections#synchronizedMap()</code> 方法来创建一个线程安全的实例：</p><figure class="highlight java"><table><tr><td class="code"><pre><code class="hljs java"><span class="hljs-type">Map</span> <span class="hljs-variable">m</span> <span class="hljs-operator">=</span> Collections.synchronizedMap(<span class="hljs-keyword">new</span> <span class="hljs-title class_">HashMap</span>(...));<br></code></pre></td></tr></table></figure><p><code>HashMap</code> 的迭代器在创建之后，只能通过 <code>iterator</code> 自己的 <code>remove</code> 方法对元素进行删除，除此以外的元素变动，都将导致使用 <code>iterator</code> 时抛出 <code>ConcurrentModificationException</code> 异常。</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p><code>HashMap</code> 在内部是由桶 (<code>bin</code> 或 <code>bucket</code>) 组成的一个哈希表，当桶变大时，将转换为 <code>TreeNodes</code>，每个 <code>TreeNodes</code> 的结构与 <code>java.util.TreeMap</code> 相似。</p><p>每个桶里的 <code>TreeNodes</code> 元素的顺序也是通过 <code>hashCode</code> 来保证，如果某两个元素实现了 <code>Comparable</code> 接口，则使用它们的 <code>compareTo</code> 方法来排序。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Go Slice</title>
      <link>https://yangshifu.xyz/posts/go-slice/</link>
      <description>
        <![CDATA[<blockquote>
<ul>
<li>原文地址：<a target="_blank" rel="noopener" href="https://blog.golang.org/slices-intro">slices-intro</a></li>
<li>原作者：Andre]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <pubDate>Fri, 21 Aug 2026 10:51:42 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://blog.golang.org/slices-intro">slices-intro</a></li><li>原作者：Andrew Gerrand</li><li>原文发表于：2011-01-5</li><li>所有图片均来源于原文</li></ul></blockquote><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>Go 语言的切片类型提供了方便且高效的方式来操作 <strong>类型化的数据序列</strong>。切片类型除了一些特殊的属性外，与其他程序语言的数组非常相似。本文将研究切片是什么以及如何使用它。</p><h2 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h2><p>切片类型是建立在数组之上的抽象概念，所以在深入切片之前我们首先需要了解数组。</p><p>数组的定义需要指定数组长度及元素类型。例如：<code>[4]int</code> 类型表示一个由 <code>4</code> 个整数组成的数组。数组的大小是固定的，长度是数组类型的一部分——<code>[4]int</code> 和 <code>[5]int</code> 是不同且不兼容的两个类型。可以使用常规的下标来索引数组，即 <code>s[n]</code> 访问数组 <code>s</code> 中从 <code>0</code> 开始的第 <code>n</code> 个元素。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">var</span> a [<span class="hljs-number">4</span>]<span class="hljs-type">int</span><br>a[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span><br>i := a[<span class="hljs-number">0</span>]<br><span class="hljs-comment">// i == 1</span><br></code></pre></td></tr></table></figure><p>数组不需要显式的初始化；零值数组中的元素都已经被初始化为对应类型的零值，可以直接使用。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// a[2] == 0, int 类型的零值</span><br></code></pre></td></tr></table></figure><p><code>[4]int</code> 在内存中表示为 <strong>连续存储的 4 个整数</strong>：</p><p><img src="/posts/go-slice/images/6a099a0d75c1a4387879d0d3cb00404d.png" alt="slice-array.png" loading="lazy"></p><p>Go 语言的数组是一个值。一个数组变量表示的是整个数组；而不是指向第一个元素的指针（比如 C 语言）。这就意味着当你对数组变量进行赋值或当作参数传递时，其接收方实际上是获得了该数组的一个 <strong>副本</strong>。（为了避免数组的复制，可以在传递时使用指向数组的指针，不过那就不是我们正在讨论的数组，而是 <strong>数组的指针</strong> 了。）我们可以将数组看作一种包含索引而不是命名字段的结构体（struct）：一个固定大小的复合值。</p><p>数组可以有如下的字面量定义：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">b := [<span class="hljs-number">2</span>]<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;Penn&quot;</span>, <span class="hljs-string">&quot;Teller&quot;</span>&#125;<br></code></pre></td></tr></table></figure><p>或者可以让编译器来决定数组长度：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">b := [...]<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;Penn&quot;</span>, <span class="hljs-string">&quot;Teller&quot;</span>&#125;<br></code></pre></td></tr></table></figure><p>无论以上哪种定义方式，<code>b</code> 都将是 <code>[2]string</code> 类型。</p><h2 id="切片"><a href="#切片" class="headerlink" title="切片"></a>切片</h2><p>数组有适用它们的地方，不过由于它们不够灵活，所以在 Go 的代码中并不常见。而切片却随处可见。切片基于数组，拥有更强的功能，也更便于使用。</p><p>切片的类型规范是 <code>[]T</code>，其中 <code>T</code> 代表切片元素的类型。与数组不同的是，切片类型没有指定长度。</p><p>切片的字面量定义除了长度之外与数组一致：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">letters := []<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;a&quot;</span>, <span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-string">&quot;c&quot;</span>, <span class="hljs-string">&quot;d&quot;</span>&#125;<br></code></pre></td></tr></table></figure><p>切片还可以用内置的 <code>make</code> 函数来创建：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">make</span><span class="hljs-params">([]T, <span class="hljs-built_in">len</span>, <span class="hljs-built_in">cap</span>)</span></span> []T<br></code></pre></td></tr></table></figure><p>其中 <code>T</code> 依然代表即将创建的切片元素类型。<code>make</code> 函数接受 3 个参数：<strong>类型</strong>、<strong>长度（len）</strong>、及 <strong>可选的容量（cap）</strong>。当使用 <code>make</code> 函数创建切片时，它在内部创建了一个数组，并返回了一个指向该数组的切片。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">var</span> s []<span class="hljs-type">byte</span><br>s = <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-number">5</span>, <span class="hljs-number">5</span>)<br><span class="hljs-comment">// s == []byte&#123;0, 0, 0, 0, 0&#125;</span><br></code></pre></td></tr></table></figure><p>不提供容量（<code>cap</code>）参数时，其默认值与长度（<code>len</code>）一致：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">s := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-number">5</span>)<br><span class="hljs-comment">// s == []byte&#123;0, 0, 0, 0, 0&#125;</span><br></code></pre></td></tr></table></figure><p>可以使用内置的 <code>len</code> 和 <code>cap</code> 函数来查看切片的长度与容量：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// len(s) == 5</span><br><span class="hljs-comment">// cap(s) == 5</span><br></code></pre></td></tr></table></figure><p>接下来的两个部分将讨论 <strong>长度</strong> 与 <strong>容量</strong> 之间的关系。</p><p>切片类型的零值是 <code>nil</code>。<code>len</code> 和 <code>cap</code> 函数在零值切片上都将返回 <code>0</code>。</p><p>切片可以由已经声明的切片或数组分割而来。两个索引加上一个分号（<code>:</code>）即可实现分割。比如：表达式 <code>b[1:4]</code> 创建了一个包含 <code>b</code> 中 <code>1</code> 到 <code>3</code> 三个元素的切片（在分割后的切片中下标为 <code>0</code> 到 <code>2</code>）。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">b := []<span class="hljs-type">byte</span>&#123;<span class="hljs-string">&#x27;g&#x27;</span>, <span class="hljs-string">&#x27;o&#x27;</span>, <span class="hljs-string">&#x27;l&#x27;</span>, <span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;n&#x27;</span>, <span class="hljs-string">&#x27;g&#x27;</span>&#125;<br><span class="hljs-comment">// b[1:4] == []byte&#123;&#x27;o&#x27;, &#x27;l&#x27;, &#x27;a&#x27;&#125;, 其底层数组与 b 一致</span><br></code></pre></td></tr></table></figure><p>在进行切片分割的表达式中，索引可以省略。在省略时，开始的索引默认值为 <code>0</code>，结束的索引默认值为 <strong>切片的长度（len）</strong> ：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">b := []<span class="hljs-type">byte</span>&#123;<span class="hljs-string">&#x27;g&#x27;</span>, <span class="hljs-string">&#x27;o&#x27;</span>, <span class="hljs-string">&#x27;l&#x27;</span>, <span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;n&#x27;</span>, <span class="hljs-string">&#x27;g&#x27;</span>&#125;<br><span class="hljs-comment">// b[:2] == []byte&#123;&#x27;g&#x27;, &#x27;o&#x27;&#125;</span><br><span class="hljs-comment">// b[2:] == []byte&#123;&#x27;l&#x27;, &#x27;a&#x27;, &#x27;n&#x27;, &#x27;g&#x27;&#125;</span><br><span class="hljs-comment">// b[:] == b</span><br><span class="hljs-comment">// 以下为译者注：</span><br><span class="hljs-comment">// b[:2] == b[0:2]</span><br><span class="hljs-comment">// b[2:] == b[2:len(b)]</span><br><span class="hljs-comment">// b[:] == b[0:len(b)] == b</span><br></code></pre></td></tr></table></figure><p>还可以使用一个数组来创建切片：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">x := [<span class="hljs-number">3</span>]<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;Лайка&quot;</span>, <span class="hljs-string">&quot;Белка&quot;</span>, <span class="hljs-string">&quot;Стрелка&quot;</span>&#125;<br>s := x[:] <span class="hljs-comment">// s 切片的底层为 x 数组</span><br></code></pre></td></tr></table></figure><h2 id="切片的本质"><a href="#切片的本质" class="headerlink" title="切片的本质"></a>切片的本质</h2><p>切片实际描述的是一个数组的片段，它由 <code>3</code> 部分组成：指向数组的指针、数组片段的长度以及容量（数组片段的最大长度）。</p><p><img src="/posts/go-slice/images/6b0d470a2ac19fb72349214f1dc3af1d.png" alt="slice-struct.png" loading="lazy"></p><p>我们之前通过 <code>make([]byte, 5)</code> 表达式创建的切片，其结构表示如下：</p><p><img src="/posts/go-slice/images/dd2915bebafe287f84565119442b8630.png" alt="slice-1.png" loading="lazy"></p><p>长度是切片引用的元素个数。容量是切片底层数组的元素个数（从切片指针指向的元素开始）。接下来的几个例子将阐述长度和容量的不同之处。</p><p>我们将一边分割切片 <code>s</code>，一边观察其内部的数据结构及它们和底层数组之间的关联：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">s = s[<span class="hljs-number">2</span>:<span class="hljs-number">4</span>]<br></code></pre></td></tr></table></figure><p><img src="/posts/go-slice/images/36c37bf9f7e9243b9143a7d2c1fb8066.png" alt="slice-2.png" loading="lazy"></p><p>分割过程并未复制切片的数据。只是创建了一个新的切片指向了原有的数组。这使得分割操作就如同数组下标操作一样高效。因此，修改新切片中的数据会影响旧切片。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">d := []<span class="hljs-type">byte</span>&#123;<span class="hljs-string">&#x27;r&#x27;</span>, <span class="hljs-string">&#x27;o&#x27;</span>, <span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;d&#x27;</span>&#125;<br>e := d[<span class="hljs-number">2</span>:]<br><span class="hljs-comment">// e == []byte&#123;&#x27;a&#x27;, &#x27;d&#x27;&#125;</span><br>e[<span class="hljs-number">1</span>] = <span class="hljs-string">&#x27;m&#x27;</span><br><span class="hljs-comment">// e == []byte&#123;&#x27;a&#x27;, &#x27;m&#x27;&#125;</span><br><span class="hljs-comment">// d == []byte&#123;&#x27;r&#x27;, &#x27;o&#x27;, &#x27;a&#x27;, &#x27;m&#x27;&#125;</span><br></code></pre></td></tr></table></figure><p>之前的分割操作都没有使切片长度大于它的容量。我们也可以通过分割操作扩容 <code>s</code>：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">s = s[:<span class="hljs-built_in">cap</span>(s)]<br></code></pre></td></tr></table></figure><p><img src="/posts/go-slice/images/8cdf8658177b3f3acda178b4423d4eb4.png" alt="slice-3.png" loading="lazy"></p><p>切片扩容时长度不能超过其容量。尝试这样做的时候将会引发运行时崩溃（<code>runtime panic</code>），和越界访问一个切片或数组一样。同样的，切片的长度也不能被分割到小于 <code>0</code>。</p><h2 id="切片的扩容（copy-和-append-函数）"><a href="#切片的扩容（copy-和-append-函数）" class="headerlink" title="切片的扩容（copy 和 append 函数）"></a>切片的扩容（<code>copy</code> 和 <code>append</code> 函数）</h2><p>切片的扩容实际上是创建一个新的更大的切片，然后将数据拷贝到其中。这种技术就是其他程序语言中的动态数据的实际实现方式。下边的例子将创建一个两倍大小于切片 <code>s</code> 的切片 <code>t</code> 、从 <code>s</code> 拷贝内容到 <code>t</code>，最后将 <code>t</code> 的值赋值给 <code>s</code>：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">t := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-built_in">len</span>(s), (<span class="hljs-built_in">cap</span>(s)+<span class="hljs-number">1</span>)*<span class="hljs-number">2</span>) <span class="hljs-comment">// +1 in case cap(s) == 0</span><br><span class="hljs-keyword">for</span> i := <span class="hljs-keyword">range</span> s &#123;<br>        t[i] = s[i]<br>&#125;<br>s = t<br></code></pre></td></tr></table></figure><p>循环部分的代码可以更简单地由内置函数 <code>copy</code> 来替代。如同它的名字一样，<code>copy</code> 函数复制源切片的数据到目标切片，然后返回复制的元素个数。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">copy</span><span class="hljs-params">(dst, src []T)</span></span> <span class="hljs-type">int</span><br></code></pre></td></tr></table></figure><p><code>copy</code> 函数支持复制不同长度的切片（复制的元素个数取决于两个切片中长度最小的那个）。此外，<code>copy</code> 函数可以正确处理源切片和目标切片为同一个底层数组，即切片重叠的情况。</p><p>使用 <code>copy</code> 函数，上边的代码可以简化为：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">t := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-built_in">len</span>(s), (<span class="hljs-built_in">cap</span>(s)+<span class="hljs-number">1</span>)*<span class="hljs-number">2</span>)<br><span class="hljs-built_in">copy</span>(t, s)<br>s = t<br></code></pre></td></tr></table></figure><p>另一个切片的常见操作是向其尾部追加元素。下面的函数将 <code>byte</code> 类型的元素追加到一个 <code>[]byte</code> 切片，有必要还会进行扩容，并返回追加元素后的切片：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">AppendByte</span><span class="hljs-params">(slice []<span class="hljs-type">byte</span>, data ...<span class="hljs-type">byte</span>)</span></span> []<span class="hljs-type">byte</span> &#123;<br>    m := <span class="hljs-built_in">len</span>(slice)<br>    n := m + <span class="hljs-built_in">len</span>(data)<br>    <span class="hljs-keyword">if</span> n &gt; <span class="hljs-built_in">cap</span>(slice) &#123; <span class="hljs-comment">// 如果有必要则进行扩容</span><br>        <span class="hljs-comment">// 双倍长度扩容</span><br>        newSlice := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, (n+<span class="hljs-number">1</span>)*<span class="hljs-number">2</span>)<br>        <span class="hljs-built_in">copy</span>(newSlice, slice)<br>        slice = newSlice<br>    &#125;<br>    slice = slice[<span class="hljs-number">0</span>:n]<br>    <span class="hljs-built_in">copy</span>(slice[m:n], data)<br>    <span class="hljs-keyword">return</span> slice<br>&#125;<br></code></pre></td></tr></table></figure><p>然后我们可以这样使用 <code>AppendByte</code> 函数：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">p := []<span class="hljs-type">byte</span>&#123;<span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">5</span>&#125;<br>p = AppendByte(p, <span class="hljs-number">7</span>, <span class="hljs-number">11</span>, <span class="hljs-number">13</span>)<br><span class="hljs-comment">// p == []byte&#123;2, 3, 5, 7, 11, 13&#125;</span><br></code></pre></td></tr></table></figure><p>类似 <code>AppendByte</code> 的一些工具函数非常有用，因为它完整地处理了追加元素时切片的扩容操作。它可以根据程序的特点来决定扩容的大小，或设定扩容的阈值。</p><p>但是大多数的程序不需要如此完整的控制，所以 Go 语言提供了一个内置的 <code>append</code> 函数来在大多数情况下直接使用，下面是它的方法签名：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">append</span><span class="hljs-params">(s []T, x ...T)</span></span> []T<br></code></pre></td></tr></table></figure><p><code>append</code> 函数追加元素到切片的尾部，并在合适的时候对切片进行扩容。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">a := <span class="hljs-built_in">make</span>([]<span class="hljs-type">int</span>, <span class="hljs-number">1</span>)<br><span class="hljs-comment">// a == []int&#123;0&#125;</span><br>a = <span class="hljs-built_in">append</span>(a, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)<br><span class="hljs-comment">// a == []int&#123;0, 1, 2, 3&#125;</span><br></code></pre></td></tr></table></figure><p>追加一个切片到另一个切片时，使用 <code>...</code> 来展开第二个参数：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go">a := []<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Paul&quot;</span>&#125;<br>b := []<span class="hljs-type">string</span>&#123;<span class="hljs-string">&quot;George&quot;</span>, <span class="hljs-string">&quot;Ringo&quot;</span>, <span class="hljs-string">&quot;Pete&quot;</span>&#125;<br>a = <span class="hljs-built_in">append</span>(a, b...) <span class="hljs-comment">// 等同于 &quot;append(a, b[0], b[1], b[2])&quot;</span><br><span class="hljs-comment">// a == []string&#123;&quot;John&quot;, &quot;Paul&quot;, &quot;George&quot;, &quot;Ringo&quot;, &quot;Pete&quot;&#125;</span><br></code></pre></td></tr></table></figure><p>零值切片可以当作 <code>0</code> 长度的切片来使用，可以定义一个切片零值，并在循环中向其追加元素：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-comment">// Filter 返回一个只包含满足 fn 函数的元素的新切片</span><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Filter</span><span class="hljs-params">(s []<span class="hljs-type">int</span>, fn <span class="hljs-keyword">func</span>(<span class="hljs-type">int</span>)</span></span> <span class="hljs-type">bool</span>) []<span class="hljs-type">int</span> &#123;<br>    <span class="hljs-keyword">var</span> p []<span class="hljs-type">int</span> <span class="hljs-comment">// == nil</span><br>    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> s &#123;<br>        <span class="hljs-keyword">if</span> fn(v) &#123;<br>            p = <span class="hljs-built_in">append</span>(p, v)<br>        &#125;<br>    &#125;<br>    <span class="hljs-keyword">return</span> p<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="潜在的陷阱"><a href="#潜在的陷阱" class="headerlink" title="潜在的陷阱"></a>潜在的陷阱</h2><p>如同前面所说，在分割切片时并不会复制其底层的数组。整个数组会一直存在于内存中，直到它不再被引用。在偶然的情况下，这会导致程序保留整个数据在内存中，即使你只需要其中的一小部分。</p><p>如下所示，<code>FindDigits</code> 函数将一个文件加载到内存中，将其中第一个连续的数字当作一个新的切片 <code>[]byte</code> 返回到函数之外。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">var</span> digitRegexp = regexp.MustCompile(<span class="hljs-string">&quot;[0-9]+&quot;</span>)<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">FindDigits</span><span class="hljs-params">(filename <span class="hljs-type">string</span>)</span></span> []<span class="hljs-type">byte</span> &#123;<br>    b, _ := ioutil.ReadFile(filename)<br>    <span class="hljs-keyword">return</span> digitRegexp.Find(b)<br>&#125;<br></code></pre></td></tr></table></figure><p>这个函数确实只是简单地 <code>找到数字</code>，但是它返回的切片却指向了一个包含整个文件内容的数组。如此一来，只要返回的切片还在使用，垃圾回收器就无法对这个巨大的数据进行回收。几个有用的字节却让整个文件内容保留在了内存之中。</p><p>为了解决这个问题，我们可以只拷贝所需的数据到切片中，然后返回它：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CopyDigits</span><span class="hljs-params">(filename <span class="hljs-type">string</span>)</span></span> []<span class="hljs-type">byte</span> &#123;<br>    b, _ := ioutil.ReadFile(filename)<br>    b = digitRegexp.Find(b)<br>    c := <span class="hljs-built_in">make</span>([]<span class="hljs-type">byte</span>, <span class="hljs-built_in">len</span>(b))<br>    <span class="hljs-built_in">copy</span>(c, b)<br>    <span class="hljs-keyword">return</span> c<br>&#125;<br></code></pre></td></tr></table></figure><p>可以用 <code>append</code> 函数来简化上面的函数，就当作一个练习留给读者。</p><details><summary>!! DONT CLICK ME!!</summary><p>return append([]byte{}, b…)</p></details><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><p><a target="_blank" rel="noopener" href="https://golang.org/doc/effective_go.html">Effective Go</a> 包含了对 <a target="_blank" rel="noopener" href="https://golang.org/doc/effective_go.html#slices">slices</a> 和 <a target="_blank" rel="noopener" href="https://golang.org/doc/effective_go.html#arrays">arrays</a> 的更深入的探讨，并且 <a target="_blank" rel="noopener" href="https://golang.org/doc/go_spec.html">Go 语言规范</a>定义了 <a target="_blank" rel="noopener" href="https://golang.org/doc/go_spec.html#Slice_types">slices</a> 和 <a target="_blank" rel="noopener" href="https://golang.org/doc/go_spec.html#Length_and_capacity">相关的</a> <a target="_blank" rel="noopener" href="https://golang.org/doc/go_spec.html#Making_slices_maps_and_channels">工具</a> <a target="_blank" rel="noopener" href="https://golang.org/doc/go_spec.html#Appending_and_copying_slices">函数</a>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Go Interface</title>
      <link>https://yangshifu.xyz/posts/go-interface/</link>
      <description>
        <![CDATA[<blockquote>
<ul>
<li>原文地址：<a target="_blank" rel="noopener" href="https://research.swtch.com/interfaces">Go Data Structures: Interfaces</a></li>
<li>原作者：Russ Cox</li>
<li>原文发表于：2009-12-01</li>
<li>所有图片均来源于原文</li>
<li>译者注：<ul>
<li>由于本文年代久远，部分内容可能已经过时，请读者自行甄别；</li>
<li>译者在能力范围之内，会在译文中进行标注过时内容以及新的实现；</li>
<li>文中部分链接已失效，译者已尽力恢复。</li>
</ul>
</li>
</ul>
</blockquote>
<p>从语言设计的角度来看，对于我个人而言，Go 语言的接口是最令我兴奋的部分——在编译时是静态且进行类型检查的，而在运行时则是动态的。如果可以迁移一个语言特性到别的开发语言，那么必将是接口。</p>
<p>本文是我在实现 Go 语言编译器：<code>6g</code> <code>8g</code> <code>5g</code> （译者注：在 Go 1.5 实现自举之后已经不存在这些编译器，这些编译器存在于早期的 C 实现中。） 中接口值时的观点。与此同时， Ian Lance Taylor 写了两篇关于 <code>gccgo</code> 中接口值实现的文章（<a target="_blank" rel="noopener" href="http://www.airs.com/blog/archives/277">1</a> <a target="_blank" rel="noopener" href="http://www.airs.com/blog/archives/276">2</a>）。两种实现的方式几乎是相同的，最大的不同是：本文配了图。<br>
在讨论具体实现之前，让我们先了解为何要实现它。</p>]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <pubDate>Fri, 21 Aug 2026 10:47:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://research.swtch.com/interfaces">Go Data Structures: Interfaces</a></li><li>原作者：Russ Cox</li><li>原文发表于：2009-12-01</li><li>所有图片均来源于原文</li><li>译者注：<ul><li>由于本文年代久远，部分内容可能已经过时，请读者自行甄别；</li><li>译者在能力范围之内，会在译文中进行标注过时内容以及新的实现；</li><li>文中部分链接已失效，译者已尽力恢复。</li></ul></li></ul></blockquote><p>从语言设计的角度来看，对于我个人而言，Go 语言的接口是最令我兴奋的部分——在编译时是静态且进行类型检查的，而在运行时则是动态的。如果可以迁移一个语言特性到别的开发语言，那么必将是接口。</p><p>本文是我在实现 Go 语言编译器：<code>6g</code> <code>8g</code> <code>5g</code> （译者注：在 Go 1.5 实现自举之后已经不存在这些编译器，这些编译器存在于早期的 C 实现中。） 中接口值时的观点。与此同时， Ian Lance Taylor 写了两篇关于 <code>gccgo</code> 中接口值实现的文章（<a target="_blank" rel="noopener" href="http://www.airs.com/blog/archives/277">1</a> <a target="_blank" rel="noopener" href="http://www.airs.com/blog/archives/276">2</a>）。两种实现的方式几乎是相同的，最大的不同是：本文配了图。<br>在讨论具体实现之前，让我们先了解为何要实现它。</p><span id="more"></span><h2 id="用法"><a href="#用法" class="headerlink" title="用法"></a>用法</h2><p>Go 语言的接口可以让你像在其他动态语言，如 Python 语言一样使用 <a target="_blank" rel="noopener" href="http://en.wikipedia.org/wiki/Duck_typing">Duck Typing(鸭子类型)</a>，并且编译器还能辅助你捕获一些明显的错误，比如：在期望一个包含 <code>Read</code> 方法的值时却传递了一个 <code>int</code>，或者调用 <code>Read</code> 方法时参数个数错误。使用接口，首先就需要定义一个接口类型，比如下边的 <code>ReadCloser</code>：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> ReadCloser <span class="hljs-keyword">interface</span> &#123;<br>    Read(b []<span class="hljs-type">byte</span>) (n <span class="hljs-type">int</span>, err os.Error)<br>    Close()<br>&#125;<br></code></pre></td></tr></table></figure><p>然后定义一个新的函数，其参数为 <code>ReadCloser</code> 类型。比如如下函数中，重复调用 <code>Read</code> 方法去获取所有的数据，然后调用 <code>Close</code> 进行关闭：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ReadAndClose</span><span class="hljs-params">(r ReadCloser, buf []<span class="hljs-type">byte</span>)</span></span> (n <span class="hljs-type">int</span>, err os.Error) &#123;<br>    <span class="hljs-keyword">for</span> <span class="hljs-built_in">len</span>(buf) &gt; <span class="hljs-number">0</span> &amp;&amp; err == <span class="hljs-literal">nil</span> &#123;<br>        <span class="hljs-keyword">var</span> nr <span class="hljs-type">int</span><br>        nr, err = r.Read(buf)<br>        n += nr<br>        buf = buf[nr:]<br>    &#125;<br>    r.Close()<br>    <span class="hljs-keyword">return</span><br>&#125;<br></code></pre></td></tr></table></figure><p>上述代码中的 <code>ReadAndClose</code> 函数，接收任意实现了 <code>Read</code> 和 <code>Close</code> 方法的类型，只要它的方法签名正确。与 Python 等其他语言不一样的是，如果你传递了错误的类型，那么编译时就会爆出错误，而不是在运行时。</p><p>但是接口也不仅限于静态检查，也可以动态地检查某个接口值是否包含指定的方法。比如：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Stringer <span class="hljs-keyword">interface</span> &#123;<br>    String() <span class="hljs-type">string</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ToString</span><span class="hljs-params">(any <span class="hljs-keyword">interface</span>&#123;&#125;)</span></span> <span class="hljs-type">string</span> &#123;<br>    <span class="hljs-keyword">if</span> v, ok := any.(Stringer); ok &#123;<br>        <span class="hljs-keyword">return</span> v.String()<br>    &#125;<br>    <span class="hljs-keyword">switch</span> v := any.(<span class="hljs-keyword">type</span>) &#123;<br>    <span class="hljs-keyword">case</span> <span class="hljs-type">int</span>:<br>        <span class="hljs-keyword">return</span> strconv.Itoa(v)<br>    <span class="hljs-keyword">case</span> float:<br>        <span class="hljs-keyword">return</span> strconv.Ftoa(v, <span class="hljs-string">&#x27;g&#x27;</span>, <span class="hljs-number">-1</span>)<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;???&quot;</span><br>&#125;<br></code></pre></td></tr></table></figure><p><code>any</code> 参数定义为 <code>interface{}</code>，这就意味着它不保证它其中包含什么方法，因为它可以是任何类型。在 <code>if</code> 语句中的 <code>comma ok</code> （译者注：在这里指的是上述代码中的 <code>v, ok := ...</code>） 赋值方式，要求 <code>any</code> 可以被转换为包含 <code>String()</code> 方法的 <code>Stringer</code> 类型。如果 <code>ok == true</code>，那么在接下来的语句中就直接调用 <code>String()</code> 方法并返回。此外，在函数结束之前，<code>switch</code> 语句对 <code>any</code> 的类型进行了条件选择。这个函数可以看作 <a target="_blank" rel="noopener" href="http://golang.org/pkg/fmt/">fmt 包</a> 的一个精简版本。（正常来说，上述代码中的 <code>if</code> 段可以被在 <code>switch</code> 条件中增加一个 <code>case Stringer</code> 来替换，这里展示了另一种值得注意的明确做法。）</p><p>一个简单的例子，为 64 位整数定义一个 <code>String</code> 方法来输出它的二进制值，定义一个 <code>Get</code> 方法来返回它的实际值：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Binary <span class="hljs-type">uint64</span><br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> String() <span class="hljs-type">string</span> &#123;<br>    <span class="hljs-keyword">return</span> strconv.Uitob64(i.Get(), <span class="hljs-number">2</span>)<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> Get() <span class="hljs-type">uint64</span> &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-type">uint64</span>(i)<br>&#125;<br></code></pre></td></tr></table></figure><p><code>Binary</code> 类型可以被传递到 <code>ToString</code> 函数中，然后该函数使用 <code>Binary</code> 的 <code>String</code> 方法来格式化输出，即使程序员从来没有显式地声明 <code>Binary</code> 类型实现了 <code>Stringer</code> 接口。因为这是不需要的：Go 语言的运行时会看到 <code>Binary</code> 类型包含了 <code>String</code> 方法，所以它就实现了 <code>Stringer</code> 接口，即便 <code>Binary</code> 类型的作者甚至都没有听说过 <code>Stringer</code> 接口。</p><p>这些示例表明，即使在编译时检查所有隐式转换，显式接口间转换也可以在运行时查询方法集。<a target="_blank" rel="noopener" href="http://golang.org/doc/effective_go.html#interfaces">Effective Go</a> 中包含更多接口值的详细使用示例。</p><h2 id="接口值-interface-values"><a href="#接口值-interface-values" class="headerlink" title="接口值 (Interface Values)"></a>接口值 (Interface Values)</h2><p>具有方法的语言通常有以下两种实现：一是准备一个方法表来静态调用（如 C++ 和 Java），二是在调用时进行方法查找（如 Smalltalk、Javascript 和 Python），然后缓存起来以提高调用效率。Go 语言则位于两种实现之间，它具有方法表，但在运行时对其计算。我不知道 Go 是否是第一个使用此技术的语言，但这种方式确实不常见。</p><p><strong>以下所有操作假定在 32 位机器上执行。</strong></p><p>在上面的例子中 <code>Binary</code> 类型是由两个 <code>32-bit</code> 字组成的 <code>64-bit</code> 整数：</p><p><img src="/posts/go-interface/images/2dcf8923d193583b48dcaef78a4929a9.png" alt="gointer1.png" loading="lazy"></p><p>接口类型的值在内存中表现为两个字组成的一对数据，其中一个字为指向类型信息的指针，另一个则是指向关联值的指针。将 <code>b</code> 赋值给接口类型的值时会设置这两个指针：</p><p><img src="/posts/go-interface/images/6ae6ea6bd5f339946d65bff975ffc1d0.png" alt="gointer2.png" loading="lazy"></p><p>(上图中灰色的箭头强调说明这些指针是隐式的，在 Go 程序中不可见的。)</p><p>接口值中的第一个 <code>32-bit</code> 字指向了 <code>interface table</code> 或称为 <code>itable</code>（读音为 <code>i-table</code>；在<a target="_blank" rel="noopener" href="https://github.com/golang/go/blob/weekly.2009-12-07/src/pkg/runtime/iface.c#L24">源码的 C 实现</a>中，其名称为 <code>Itab</code>）。<code>itable</code> 的头部是一些关于类型的元数据，然后是指向函数的指针列表。注意这里的 <code>itable</code> 只对应 <code>interface</code> 类型，而不是动态的接口类型。就我们的例子而言，<code>Stringer</code> 的 <code>itable</code> 中仅包含了适配 <code>Binary</code> 类型中适配 <code>Stringer</code> 接口的 <code>String</code> 方法，<code>Binary</code> 的其他方法在此 <code>itable</code> 中不可见，如 <code>Get</code>。</p><p>接口值中的第二个 <code>32-bit</code> 字指向了实际的值，即 <code>b</code> 的副本。就像 <code>var c uint64 = b</code> 创建了 <code>b</code> 的一个副本一样，<code>var s Stringer = b</code> 同样也创建了一个 <code>b</code> 的副本：如果 <code>b</code> 发生了改变，那么 <code>s</code> 和 <code>c</code> 不会受到影响。存储在接口中的值可能为任意大小，但是只有一个 <code>32-bit</code> 字用于保存接口结构中的值，因此在内存分配时，堆上创建了一块内存，并将指针记录在这个 <code>32-bit</code> 字中。（接口值在某些时候会进行优化，后面会谈到。）</p><p>当在检查一个接口值是否匹配一个类型时，比如上述代码中的 <code>switch</code>，Go 编译器生成了类似 <code>s.tab-&gt;type</code> 这样的 C 代码来获取类型的指针并将其与期望的类型做比较。如果类型匹配，则接口值 <code>s.data</code> 就会被通过解引用进行拷贝。</p><p>为了实现 <code>s.String()</code> 方法的调用，Go 编译器生成了类似 <code>s.tab-&gt;fun[0](s.data)</code> 这样的 C 语言代码：它在 <code>itable</code> 中找到合适的函数指针，并将接口值的数据当作函数的第一个参数传入。你可以使用 <code>8g -S x.go</code> 来查看生成的代码（译者注：现在的命令已经变更为：<code>go tool compile -S x.go</code>）。请注意，在调用 <code>itable</code> 中的方法时传入的是接口值的 <code>s.data</code> 指针，而不是它实际指向的值。一般来说，在接口的调用端并不知道 <code>s.data</code> 的实际含义以及它里边到底存了什么。相反的，接口代码的实现使得 <code>itable</code> 中的函数指针仅期望一个 <code>32-bit</code> 形式的参数值。因此，示例中的函数指针实际上是 <code>(*Binary).String</code> 而不是 <code>Binary.String</code>。</p><p>目前的示例只考虑了接口只有一个方法的情况。拥有多个方法的接口，在其 <code>itable</code> 中会有同样数量的函数指针。</p><h2 id="itable-的计算"><a href="#itable-的计算" class="headerlink" title="itable 的计算"></a>itable 的计算</h2><p>现在我们已经知道 <code>itable</code> 长什么样子，但是它们是从哪儿来的？Go 语言的动态类型转换说明了并不是编译器或者链接器预先计算了它们：因为有太多的对应关系（接口类型和具体类型），而且其中的大多数并不需要。实际的做法是，编译器为每一个具体类型生成了类型描述的数据结构，比如：<code>Binary</code> <code>int</code> 或 <code>func(map[int]string)</code>。除了其他元数据之外，类型描述的数据结构中包含了一个该类型实现了哪些方法的列表。同样的，编译器也为每一个接口类型，如 <code>Stringer</code>，生成了一个不同的类型描述的数据结构，其中也同样包含了这个列表。接口的运行时通过检查具体类型的 <code>itable</code> 中的每个方法来计算接口的 <code>itable</code>。运行时会缓存它找到的每一个方法，所以这个计算工作只需要进行一次。</p><p>在我们的例子中，<code>Binary</code> 的 <code>itable</code> 中有两个方法，而 <code>Stringer</code> 的 <code>itable</code> 中只有一个。假设我们将接口的方法数定义为 <code>ni</code>，具体类型的方法数定义为 <code>nt</code>，那么整个查询映射的过程的时间复杂度为 <code>O(ni x nt)</code>，但有优化的空间。对这两个 <code>itable</code> 排序然后并行地处理它们，那么<a target="_blank" rel="noopener" href="https://github.com/golang/go/blob/weekly.2009-11-17/src/pkg/runtime/iface.c#L98">映射过程</a>的时间复杂度会降为 <code>O(ni + nt)</code>。</p><h2 id="内存优化"><a href="#内存优化" class="headerlink" title="内存优化"></a>内存优化</h2><p>在上面描述的实现中，有两种互补的方法可以优化内存空间的使用。</p><p>第一种方法，如果接口类型是空的，也就是不包含任何方法，那么 <code>itable</code> 除了保存类型之外就没有其他的作用。在这种情况下，<code>itable</code> 会被丢弃，而接口值中原本指向 <code>itable</code> 的第一个 <code>32-bit</code> 字转为直接指向其对应的类型：</p><p><img src="/posts/go-interface/images/d68e14b48e113bcccaaa1c7d9e9f3a5e.png" alt="gointer3.png" loading="lazy"></p><p>接口类型是否拥有方法，这是一个静态属性。因为在源码中的定义是有区别的：<code>interface{}</code> 及 <code>interface{ methods...}</code>，由此编译器就能区分它们是哪一种表现形式。</p><p>第二种方法，如果一个接口关联的值可以由一个机器字表示，那么就不用引入复杂的间接寻址或堆的分配。如果我们定义一个与 <code>Binary</code> 类似，但是内部实现是 <code>uint32</code> 的类型，那么接口值的实际数据值将直接保存在接口值的第二个 <code>32-bit</code> 字中：</p><p><img src="/posts/go-interface/images/259ef1823a38b8b4c000374e096c2f4a.png" alt="gointer4.png" loading="lazy"></p><p>实际数据值是直接保存在接口值中还是使用指针来指向，取决于其类型的大小。编译器针对传入的字，会安排合适的 <code>itable</code> 中的函数来进行处理。如果接收类型满足一个字，那么就会直接使用它；如果不满足，则对其解引用。如上面这些图所示，<code>Binary</code> 的方法在 <code>itable</code> 表现为 <code>(*Binary).String</code>，而在 <code>Binary32</code> 中为 <code>Binary32.String</code>，而不是 <code>(*Binary32).String</code>。</p><p>当然，接口为空且其值能用一个 <code>32-bit</code> 字表示时，会同时得到两种方法的优化：</p><p><img src="/posts/go-interface/images/96136ce7db5414c687c4c658f8f6d64f.png" alt="gointer5.png" loading="lazy"></p><h2 id="方法查询性能"><a href="#方法查询性能" class="headerlink" title="方法查询性能"></a>方法查询性能</h2><p>Smalltalk 语言和其他一些动态语言，会在每次方法调用的时候进行方法查找。为了速度，许多语言的实现都是这样的：在每个调用方使用一个单条目的缓存，通常就在指令流中。在一个多线程的程序中，这些缓存就需要小心地进行管理，因为多线程程序可能会同时进行调用。即使忽略竞态条件，缓存也会使得内存成为争夺的焦点。因为 Go 语言具有与动态方法查找一起进行的静态类型的优势，即数据值已经保存在接口之中，就可以将方法查找移到调用方这边。比如下列代码所示：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-number">1</span>   <span class="hljs-keyword">var</span> any <span class="hljs-keyword">interface</span>&#123;&#125;  <span class="hljs-comment">// initialized elsewhere</span><br><span class="hljs-number">2</span>   s := any.(Stringer)  <span class="hljs-comment">// dynamic conversion</span><br><span class="hljs-number">3</span>   <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++ &#123;<br><span class="hljs-number">4</span>       fmt.Println(s.String())<br><span class="hljs-number">5</span>   &#125;<br></code></pre></td></tr></table></figure><p>在 Go 语言中，<code>itable</code> 在第 <code>2</code> 行赋值时进行了计算；在随后第 <code>4</code> 行调用 <code>s.String()</code> 就只需进行内存获取操作，然后执行单条指令 <code>call</code> 即可。（译者注：这就是上文提到的将方法查找移到调用方）</p><p>与此形成对比的是，在其他动态语言如 Smalltalk 中，将会在第 <code>4</code> 行的循环中做很多无用的方法查找操作。由于缓存的存在会使得开销降低一些，但依然无法像单条 <code>call</code> 指令那样高效。</p><p>当然，这只是一篇博客文章，我没有任何的数字来为此论据背书，但是减少内存争夺确实是高度并行程序中的一大胜利，因为能将方法查找从密集的循环中移出。另外，我这里讨论的是一般架构，而不是具体的某个实现：将来可能还会有一些恒定因子优化（译者注：可参考 <a target="_blank" rel="noopener" href="https://en.wikipedia.org/wiki/Optimizing_compiler#Factors_affecting_optimization">Optimizing Compiler#Factors affecting optimization</a>）。</p><h2 id="更多的信息"><a href="#更多的信息" class="headerlink" title="更多的信息"></a>更多的信息</h2><p>接口运行时的实现位于，<a target="_blank" rel="noopener" href="https://github.com/golang/go/blob/weekly.2009-11-17/src/pkg/runtime/iface.c">iface.c</a>。还有很多关于接口的事情没有提到（我们甚至还没看到包含 <code>pointer receiver</code> 的示例（译者注：如 <code>func (b *Binary) somefunc()</code>））以及 <code>type</code> 描述符（除了运行时，还增强了反射能力），这些会在将来的文章中讨论。</p><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>配套的代码(<code>x.go</code>)：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-number">1</span>  <span class="hljs-keyword">package</span> main<br><span class="hljs-number">2</span><br><span class="hljs-number">3</span>  <span class="hljs-keyword">import</span> (<br><span class="hljs-number">4</span>    <span class="hljs-string">&quot;fmt&quot;</span><br><span class="hljs-number">5</span>    <span class="hljs-string">&quot;strconv&quot;</span><br><span class="hljs-number">6</span>  )<br><span class="hljs-number">7</span><br><span class="hljs-number">8</span>  <span class="hljs-keyword">type</span> Stringer <span class="hljs-keyword">interface</span> &#123;<br><span class="hljs-number">9</span>    String() <span class="hljs-type">string</span><br><span class="hljs-number">10</span>  &#125;<br><span class="hljs-number">11</span><br><span class="hljs-number">12</span> <span class="hljs-keyword">type</span> Binary <span class="hljs-type">uint64</span><br><span class="hljs-number">13</span><br><span class="hljs-number">14</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> String() <span class="hljs-type">string</span> &#123;<br><span class="hljs-number">15</span>   <span class="hljs-keyword">return</span> strconv.Uitob64(i.Get(), <span class="hljs-number">2</span>)<br><span class="hljs-number">16</span> &#125;<br><span class="hljs-number">17</span><br><span class="hljs-number">18</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> Get() <span class="hljs-type">uint64</span> &#123;<br><span class="hljs-number">19</span>   <span class="hljs-keyword">return</span> <span class="hljs-type">uint64</span>(i)<br><span class="hljs-number">20</span> &#125;<br><span class="hljs-number">21</span><br><span class="hljs-number">22</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br><span class="hljs-number">23</span>   b := Binary(<span class="hljs-number">200</span>)<br><span class="hljs-number">24</span>   s := Stringer(b)<br><span class="hljs-number">25</span>   fmt.Println(s.String())<br><span class="hljs-number">26</span> &#125;<br></code></pre></td></tr></table></figure><p>节选部分 <code>8g -S x.go</code> 的输出（译者注：<code>8g</code> 编译器已不存在，现在使用命令实现同等效果：<code>go tool compile -S x.go</code>）：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">0045 (x.go:25) LEAL    s+-24(SP),BX<br>0046 (x.go:25) MOVL    4(BX),BP<br>0047 (x.go:25) MOVL    BP,(SP)<br>0048 (x.go:25) MOVL    (BX),BX<br>0049 (x.go:25) MOVL    20(BX),BX<br>0050 (x.go:25) CALL    ,BX<br></code></pre></td></tr></table></figure><p><code>LEAL</code> 指令加载 <code>s</code> 的地址到寄存器 <code>BX</code> 中。（<code>n(SP)</code> 表示在内存中 <code>SP+n</code> 的字。<code>0(SP)</code> 可缩写为 <code>(SP)</code>。）接下来的两条 <code>MOVL</code> 指令获取接口的第二个 <code>32-bit</code> 字到寄存器 <code>BP</code> 中，然后将其保存为函数调用的第一个参数，即 <code>0(SP)</code>。最后两条 <code>MOVL</code> 指令获取 <code>itable</code> 地址，然后获取指向 <code>itable</code> 中对应函数的地址，为最终的调用做准备。</p><h2 id="译者注"><a href="#译者注" class="headerlink" title="译者注"></a>译者注</h2><p>在 Go 1.14.4 中，<code>strconv.Uitob64</code> 函数已不存在，下面是替换后的代码：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-number">1</span>  <span class="hljs-keyword">package</span> main<br><span class="hljs-number">2</span><br><span class="hljs-number">3</span>  <span class="hljs-keyword">import</span> (<br><span class="hljs-number">4</span>  <span class="hljs-string">&quot;fmt&quot;</span><br><span class="hljs-number">5</span>  <span class="hljs-string">&quot;strconv&quot;</span><br><span class="hljs-number">6</span>  )<br><span class="hljs-number">7</span><br><span class="hljs-number">8</span>  <span class="hljs-keyword">type</span> Stringer <span class="hljs-keyword">interface</span> &#123;<br><span class="hljs-number">9</span> String() <span class="hljs-type">string</span><br><span class="hljs-number">10</span> &#125;<br><span class="hljs-number">11</span><br><span class="hljs-number">12</span> <span class="hljs-keyword">type</span> Binary <span class="hljs-type">uint64</span><br><span class="hljs-number">13</span><br><span class="hljs-number">14</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> String() <span class="hljs-type">string</span> &#123;<br><span class="hljs-number">15</span> <span class="hljs-keyword">return</span> strconv.FormatUint(i.Get(), <span class="hljs-number">2</span>)<br><span class="hljs-number">16</span> &#125;<br><span class="hljs-number">17</span><br><span class="hljs-number">18</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(i Binary)</span></span> Get() <span class="hljs-type">uint64</span> &#123;<br><span class="hljs-number">19</span> <span class="hljs-keyword">return</span> <span class="hljs-type">uint64</span>(i)<br><span class="hljs-number">20</span> &#125;<br><span class="hljs-number">21</span><br><span class="hljs-number">22</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> &#123;<br><span class="hljs-number">23</span> b := Binary(<span class="hljs-number">200</span>)<br><span class="hljs-number">24</span> s := Stringer(b)<br><span class="hljs-number">25</span> fmt.Println(s.String())<br><span class="hljs-number">26</span> &#125;<br></code></pre></td></tr></table></figure><p>对应的 <code>go tool compile -s</code> 的汇编代码（已移除垃圾回收相关的指令）：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><code class="hljs plaintext">00046 (x.go:25)LEAQgo.itab.&quot;&quot;.Binary,&quot;&quot;.Stringer(SB), AX<br>00053 (x.go:25)TESTBAL, (AX)<br>00055 (x.go:24)MOVQ8(SP), AX<br>00060 (x.go:25)MOVQgo.itab.&quot;&quot;.Binary,&quot;&quot;.Stringer+24(SB), CX<br>00067 (x.go:25)MOVQAX, (SP)<br>00071 (x.go:25)CALLCX<br></code></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>Go Data</title>
      <link>https://yangshifu.xyz/posts/go-data/</link>
      <description>本文将说明基础类型、结构体、数组和切片在内存中的样子</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <category domain="https://yangshifu.xyz/tags/golang/">golang</category>
      <pubDate>Fri, 21 Aug 2026 10:22:17 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><ul><li>原文地址：<a target="_blank" rel="noopener" href="https://research.swtch.com/godata">Go Data Structures</a></li><li>原作者：Russ Cox</li><li>原文发表于：2009-11-24</li><li>所有图片均来源于原文</li><li>译者注：由于本文年代久远，部分内容可能已经过时，请读者自行甄别。译者在能力范围之内，会在译文中进行标注。</li></ul></blockquote><p>在向新程序员解释 Go 语言时，我发现解释 Go 的值在内存中的样子非常有用，这有助于他们建立正确的直觉来判断哪些操作开销大，哪些操作开销小。本文将说明基础类型、结构体、数组和切片。</p><h2 id="基础类型"><a href="#基础类型" class="headerlink" title="基础类型"></a>基础类型</h2><p>先看一些简单的例子：<img src="/posts/go-data/images/5924087851ce8462122238d28abc6cf3.png" alt="godata1.png" loading="lazy"></p><p>变量 <code>i</code> 的数据类型为 <code>int</code>，在内存中表示为一个 <code>32 位的字</code>。（所有这些图片显示的都是 <code>32</code> 位的内存布局；在当前的实现中，只有指针在 <code>64</code> 位机器上会大一些。<code>int</code> 依然是 <code>32</code> 位，虽然它的实现可以选择使用 <code>64</code> 位来代替。）</p><p>由于显式的转换，变量 <code>j</code> 的数据类型为 <code>int32</code>。尽管 <code>i</code> 和 <code>j</code> 有着相同的内存布局，但它们的数据类型却不同。赋值操作 <code>i = j</code> 将会导致类型错误，必须显式地进行类型转换：<code>i = int(j)</code>。</p><p>变量 <code>f</code> 的数据类型为 <code>float</code>，当前实现将其表示为 <code>32</code> 位浮点数。它与 <code>int32</code> 一样占用 <code>4</code> 个字节，但其内部布局则完全不同。</p><h2 id="结构体和指针"><a href="#结构体和指针" class="headerlink" title="结构体和指针"></a>结构体和指针</h2><p>接下来也是同样的，变量 <code>bytes</code> 的数据类型是 <code>[5]byte</code>，包含 <code>5</code> 个字节的数组。它在内存中表现为简单的 <code>5</code> 个字节，一个紧挨着另一个，和 C 语言中的数组一样。同样的，<code>primes</code> 是一个包含 <code>4</code> 个 <code>int</code> 的数组。</p><p>Go 语言，像 C 语言而不是 Java，给予程序员完整的控制而且不是通过指针。例如有如下类型定义：</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Point <span class="hljs-keyword">struct</span> &#123; X, Y <span class="hljs-type">int</span> &#125;<br></code></pre></td></tr></table></figure><p>这里定义了一个简单的结构体名为：<code>Point</code>，在内存中表现为两个相邻的整数：<img src="/posts/go-data/images/91ae9b3ad7d34fcb350750baa53e5aa8.png" alt="godata1a.png" loading="lazy"></p><p><code>Point{10, 20}</code> 这样一个复合字面量语法表示一个正确初始化的 <code>Point</code>。对这样的复合字面量取地址，表示为一个指向初始化后 <code>Point</code> 的指针。前一种定义方式表示<strong>内存中的两个 <code>字</code></strong>，而后一种定义方式表示<strong>一个指向内存中两个字的指针</strong>。</p><p>结构体中的字段在内存中被一个紧挨另一个地平铺。</p><figure class="highlight go"><table><tr><td class="code"><pre><code class="hljs go"><span class="hljs-keyword">type</span> Rect1 <span class="hljs-keyword">struct</span> &#123; Min, Max Point &#125;<br><span class="hljs-keyword">type</span> Rect2 <span class="hljs-keyword">struct</span> &#123; Min, Max *Point &#125;<br></code></pre></td></tr></table></figure><p><img src="/posts/go-data/images/7e6d5e77c4580a267f4ee91746276306.png" alt="godata1b.png" loading="lazy"></p><p><code>Rect1</code> 包含两个 <code>Point</code> 字段，其在内存中的表现形式为 <code>2 个 Point</code> 即 <code>4 个 ints</code> 在同一行中。<code>Rect2</code> 包含两个 <code>*Point</code> 字段，其表现形式为两个 <code>Point</code> 指针。</p><p>C 程序员应该不会对 <code>Point</code> 和 <code>*Point</code> 之间的区别感到惊讶，而使用 Java 语言、Python 语言或者其他别的语言的程序员或许会。通过给予程序员对基本内存布局完整的控制，Go 语言提供了能力去控制给定数据结构集合总的大小，以及内存访问模式，所有这些对于构建性能良好的系统都很重要。</p><h2 id="字符串"><a href="#字符串" class="headerlink" title="字符串"></a>字符串</h2><p>有了前边的这些铺垫，我们将继续研究更多有意思的数据类型。<img src="/posts/go-data/images/100ea8e00c243427dbf6d61a53ca9c3d.png" alt="godata2.png" loading="lazy"></p><p>（灰色箭头表示在实现中存在但在程序中不直接可见的指针。）</p><p>字符串在内存中表示为 <code>2 个字</code> 的结构，其中包含指向字符串数据的指针和字符串的长度。 由于字符串是不可变的，所以多个字符串共享相同的底层字节序列是安全的，因此分割 <code>s</code> 会导致新的一个 <code>2 个字</code> 的结构 <code>t</code>，其指针和长度与 <code>s</code> 不同，但仍指向相同的底层字节序列。 这意味着分割切片无需分配或复制即可完成，从而使字符串切片的效率与使用索引一样高效。</p><p>（顺便说一句，在 Java 和其他语言中有一个<a target="_blank" rel="noopener" href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4513622">众所周知的陷阱</a>，当对字符串进行切片以保存其中一小段时，对原始字符串的引用会导致整个原始字符串始终保留在内存中，即使我们只需要其中的一小部分。 Go 语言也有这个陷阱。 我们<a target="_blank" rel="noopener" href="http://code.google.com/p/go/source/detail?r=70fa38e5a5bb">尝试过并最终放弃</a>的另一种选择是重新分配内存并复制所需的部分，但这使字符串分割切片操作开销很大，以至于大多数程序都避免使用它。）</p><h2 id="切片"><a href="#切片" class="headerlink" title="切片"></a>切片</h2><p><img src="/posts/go-data/images/e0d235c6d4d30ce83d550bbdc502d435.png" alt="godata3.png" loading="lazy"></p><p>切片是对一个数组其中一段的引用。在内存中表现为 <code>3 个字</code> 大小的结构体，包含指向第一个元素的指针，切片的长度以及容量。其中长度是切片索引操作的上限，如 <code>x[i]</code>，容量则是分割操作的上限，如 <code>x[i:j]</code>。</p><p>如同分割字符串那样，分割一个数组也不会导致数据复制，而仅仅是创建了一个新的结构体来保存指向第一个元素的指针、长度及容量。例如上图所示：复合字面量 <code>[]int{2, 3, 5, 7, 11}</code> 创建了一个新的包含 <code>5</code> 个元素的数组，设置 <code>x</code> 切片中相应的字段，然后用 <code>x</code> 来描述这个切片。表达式 <code>x[1:3]</code> 并没有占用更多的内存空间，而只是创建了一个新的切片结构体，填充其中的数据，然后指向同样的底层数据序列。切片 <code>y</code> 中有效的<strong>索引</strong>为 <code>y[0]</code> 和 <code>y[1]</code>，而<strong>分割</strong>操作 <code>y[0:4]</code> 也是合法的表达式操作。</p><p>由于切片是多字结构，而不是指针，因此切片操作无需分配内存，甚至不需要为切片标头分配内存，通常可以将其保留在堆栈中。 这种表示形式使切片比在 C 语言中传递显式指针和长度开销要小很多。Go 语言最初将切片表示为指向上述结构的指针，但是这样做意味着每个切片操作都会分配一个新的内存对象。 即使使用快速分配器，也为垃圾收集器制造了许多不必要的工作，并且我们发现，与字符串一样，程序避免切片操作，而是希望传递显式索引。 在大多数情况下，移除间接寻址和分配将使分割切片的开销笑到足以避免传递显式索引。</p><h2 id="new-和-make"><a href="#new-和-make" class="headerlink" title="New 和 Make"></a><code>New</code> 和 <code>Make</code></h2><p>Go 语言有两个进行数据结构创建的函数：<code>new</code> 和 <code>make</code>。它们之间的区别在开始可能会造成困扰和疑惑，但理解之后这一切就变得自然了。最基本的一个区别是，<code>new(T)</code> 返回的是 <code>*T</code>，一个可以隐式解引用的指针（如下图中的黑色箭头），而 <code>make(T, args)</code> 返回一个普通的 <code>T</code>，而不是指针。<code>T</code> 中经常包含一些隐式指针（如下图中的灰色箭头）。<code>new</code> 返回一个指向零值的指针，而 <code>make</code> 返回一个复杂的结构。</p><p><img src="/posts/go-data/images/910f344874ad4363f0d6bf5b84a9a3bb.png" alt="godata4.png" loading="lazy">有一种统一两者的方法，且与 C 语言或者 C++ 语言大相径庭：使用 <code>make(*T)</code> 来返回一个指针指向刚刚创建的 <code>T</code>，这样一来 <code>new(Point)</code> 可以写为 <code>make(*Point)</code>。我们尝试过几天，但认为它与人们对分配功能的期望相差太大。</p><h2 id="下一步"><a href="#下一步" class="headerlink" title="下一步"></a>下一步</h2><p>写到这里文章似乎已经有点长了。<code>interface</code>、<code>maps</code> 以及 <code>channels</code> 将在后续的文章中阐述。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>自动内存管理</title>
      <link>https://yangshifu.xyz/posts/gc-automatic-memory-management/</link>
      <description>
        <![CDATA[<p>自动内存管理（Automatic Memory Management）是很多现代程序语言提供的功能之一。自动内存管理通常包含两个部分：一是分配，而是回收；之所以称之为<strong>自动</strong>，是因为在这些语言中无需程序员手动显式地申请或释放内存。</p>
<p]]>
      </description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/tech/">tech</category>
      <pubDate>Fri, 21 Aug 2026 10:17:55 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>自动内存管理（Automatic Memory Management）是很多现代程序语言提供的功能之一。自动内存管理通常包含两个部分：一是分配，而是回收；之所以称之为<strong>自动</strong>，是因为在这些语言中无需程序员手动显式地申请或释放内存。</p><p>比如在 C 语言中，你必须保证 <code>malloc</code> 和 <code>free</code> 是成对出现的，而由于这一切都是手动进行，那么就有可能出现问题。比如申请了内存却忘记了释放，会造成内存泄漏，而且会严重影响程序性能，因为内存管理器的管理压力很大，寻找适合大小的内存时会消耗更长时间；而且积累到一定程度之后会导致程序崩溃。而过早释放了内存，则会导致悬挂指针，而悬挂指针导致的崩溃可能会更诡异，在使用一个悬挂指针时，它指向的内存空间可能已经归还给了操作系统，可能已经被清空，更有可能已经被其他程序所使用，在这种情况下，程序的崩溃可能不会立即发生，更增加了查找问题的难度。</p><p>除了 C 语言这种直接手动显示申请和释放内存的语言之外，还有一些语言通过别的手段实现了内存的安全回收，比较新的比如 Rust 语言引入的所有权，使得代码在编译时即可被编译器自动完成正确的内存申请与释放功能。</p><p>自动内存管理在一定程度上可以解决大多数的内存泄漏和悬挂指针问题。自动内存管理引入垃圾回收机制来使得程序员无需再手动进行内存管理，成熟的垃圾回收算法能够阻止内存泄漏的发生；而且，由于释放内存也是由自动内存管理来完成，也就不会出现二次释放之类的事故，也避免了悬挂指针的问题。</p><p>常见的垃圾回收机制有以下几种：</p><ul><li>引用计数法：比如 PHP 语言使用的就是引用计数法，在 5.3 版本之前，引用计数法无法处理循环引用，而有可能导致内存泄漏； 5.3 版本之后，PHP 核心团队增强了引用计数法，在此基础上采用同步周期回收的同步算法来处理了此问题。</li><li>标记清除法：比如 Java 8 中老年代的 CMS 垃圾回收器，其算法就是标记清除，优点是速度快，程序暂停的时间短，但缺点也很明显：内存碎片化严重。Go 语言中的回收算法是增强的标记清除法。</li><li>标记整理法：比如 Java 8 中的 Parallel Scavenge 收集器，其在老年代的回收算法就是标记整理，其步骤与标记清除大致相似，只是最后清除垃圾前进行了整理，然后一并清除。</li><li>复制法：比如 Java 中的新生代的大部分收集器，都采用的复制算法，优点是快，缺点是需要两倍的空间（一半使用，一半用于复制）。</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hello World</title>
      <link>https://yangshifu.xyz/posts/hello-world/</link>
      <description>Hello World</description>
      <author>杨师傅</author>
      <category domain="https://yangshifu.xyz/categories/personal/">personal</category>
      <pubDate>Fri, 21 Aug 2026 03:43:32 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="hello-world"><a href="#hello-world" class="headerlink" title="Hello World"></a>Hello World</h3><p>博客迁移一次丢一次数据……</p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
