<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 16 章 工具与可观测性 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/</link><description>Recent content in 第 16 章 工具与可观测性 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/index.xml" rel="self" type="application/rss+xml"/><item><title>16.1 运行时死锁检查</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/deadlock/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/deadlock/</guid><description>&lt;h1 id="161-运行时死锁检查"&gt;16.1 运行时死锁检查&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;fatal error: all goroutines are asleep - deadlock!&lt;/code&gt;,几乎每个 Go 程序员都见过这条错误。它来自
运行时内置的死锁检测器。这一节先讲清它怎么判定、何时被触发，再讲一件更要紧的事：它在理论上
能保证什么、又有什么是它&lt;strong&gt;结构性地检测不了&lt;/strong&gt;的。后者是许多线上「卡死」之谜的根源，理解了它，
比记住那条错误信息更有价值。&lt;/p&gt;
&lt;p&gt;我们先说结论，再看它如何从一段不到一百行的代码里得出。运行时的死锁检测器并不维护、也从不检查
一张「谁在等谁」的等待图（wait-for graph）。它只问两个极粗的问题：还有没有 M 在运行？将来有没有
计时器会触发？两个都答「没有」，它就宣告死锁。这种粗糙不是疏忽，它恰恰决定了检测器的能力边界,
本节后半会看到，正因为没有逐资源的等待边，它&lt;strong&gt;无法&lt;/strong&gt;在一部分 goroutine 之间找出一个死锁环。&lt;/p&gt;
&lt;h2 id="1611-判据只数运行中的-m不看谁等谁"&gt;16.1.1 判据：只数运行中的 M，不看谁等谁&lt;/h2&gt;
&lt;p&gt;死锁检测逻辑藏在 &lt;code&gt;runtime/proc.go&lt;/code&gt; 的 &lt;code&gt;checkdead&lt;/code&gt; 里
（&lt;a href="../../../part3concurrency/ch09sched/sysmon"&gt;9.8&lt;/a&gt;）。它的判据可以一句话概括：&lt;strong&gt;没有任何线程
还在运行，且没有任何途径能让某个 goroutine 重新变得可运行，程序就死锁了。&lt;/strong&gt; 落到代码，「没有
线程在运行」是一次减法,用机器线程总数减去各类空闲与系统线程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;18
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// checkdead：判定是否陷入全局死锁（裁剪后的速写）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 调用时必须持有 sched.lock。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; checkdead() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 作为 c-shared / c-archive 库被宿主程序嵌入时，没有运行中的 goroutine 是正常的,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 宿主仍在跑。panicking、cgo 额外 M 等情形也各自豁免（此处略）。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; (islibrary || isarchive) &amp;amp;&amp;amp; GOARCH != &lt;span style="color:#a31515"&gt;&amp;#34;wasm&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// run = 机器线程数 − 空闲 M − 持锁空闲 M − 系统 M，即「正在运行的 M」数。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// run0 通常为 0（cgo 额外 M 存在时为 1）。只要还有 M 在运行，就不可能&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 是全局死锁，立即返回。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; run := mcount() - sched.nmidle - sched.nmidlelocked - sched.nmsys
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; run &amp;gt; run0 {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// ...（见下）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;注意这里没有出现任何「锁」「channel」「WaitGroup」的字样。&lt;code&gt;checkdead&lt;/code&gt; 不知道、也不关心每个
goroutine 究竟阻塞在哪个原语上,它只数还有几个 M 在跑。这是理解它全部行为的关键。&lt;/p&gt;</description></item><item><title>16.2 竞争检查</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/race/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/race/</guid><description>&lt;h1 id="162-竞争检查"&gt;16.2 竞争检查&lt;/h1&gt;
&lt;p&gt;数据竞争（data race）是并发程序里最隐蔽、最难复现的一类 bug。它的定义很短：两个 goroutine
并发访问同一内存地址、其中至少一个是写、且二者之间没有任何同步把它们排序，那么程序的行为
就是未定义的（&lt;a href="../../../part3concurrency/ch11sync/mem"&gt;11.9&lt;/a&gt;）。「未定义」不是「读到旧值」
这么温和，它意味着编译器与处理器都被允许做出令人意外的重排，结果可能时对时错，可能只在压力
负载下、只在某种 CPU 上、只在某个版本里偶发一次。靠人眼复查这种 bug 是没有指望的。Go 的
&lt;strong&gt;竞态检测器&lt;/strong&gt;（&lt;code&gt;-race&lt;/code&gt;）正是把这件事从「靠运气复现」变成「跑一遍就告诉你」的工具。这一节
讲清它的原理、它背后的算法代价、以及它那两条必须记住的能力边界。&lt;/p&gt;
&lt;h2 id="1621-基于-happens-before-的动态检测"&gt;16.2.1 基于 happens-before 的动态检测&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;go test -race&lt;/code&gt;、&lt;code&gt;go run -race&lt;/code&gt;、&lt;code&gt;go build -race&lt;/code&gt;、&lt;code&gt;go install -race&lt;/code&gt; 都能开启竞态检测。它的
内核是 Google 在 LLVM &lt;code&gt;compiler-rt&lt;/code&gt; 里维护的 &lt;strong&gt;ThreadSanitizer&lt;/strong&gt;（TSan）运行时，Go 以预编译的
&lt;code&gt;.syso&lt;/code&gt; 形式把它链进程序（见 &lt;code&gt;runtime/race/&lt;/code&gt;，因此 &lt;code&gt;-race&lt;/code&gt; 依赖 cgo）。它的工作原理是
&lt;strong&gt;动态的 happens-before 检测&lt;/strong&gt;：程序运行时，编译器在每一次内存访问前插入一个回调，运行时则
拦截每一个同步事件，二者合起来让 TSan 实时维护一张 happens-before 关系图（&lt;a href="../../../part3concurrency/ch11sync/mem"&gt;11.9&lt;/a&gt;
那条偏序）。当它发现两个 goroutine 访问了同一地址、至少一个是写、而二者之间&lt;strong&gt;不存在&lt;/strong&gt;任何
happens-before 边，就报告一个数据竞争。&lt;/p&gt;
&lt;p&gt;为什么「访问」与「同步」要分开拦截？因为这正是 happens-before 偏序的两个来源。访问告诉检测器
「谁在何时碰了哪块内存」，同步告诉它「哪两条时间线之间被建立了次序」。Go 在 &lt;code&gt;runtime/race.go&lt;/code&gt;
里暴露的这组钩子，恰好把这两类事件一一对应了出来：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;//go:build race&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 访问事件：编译器在每次读/写内存前自动插桩调用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceRead(addr unsafe.Pointer)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceWrite(addr unsafe.Pointer)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceReadRange(addr unsafe.Pointer, len &lt;span style="color:#2b91af"&gt;int&lt;/span&gt;) &lt;span style="color:#008000"&gt;// 切片、memmove 等成片访问&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceWriteRange(addr unsafe.Pointer, len &lt;span style="color:#2b91af"&gt;int&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 同步事件：在 addr 上建立跨 goroutine 的 happens-before 关系&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 注释原文：establish happens-before relations between goroutines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceAcquire(addr unsafe.Pointer) &lt;span style="color:#008000"&gt;// ≈ C11 atomic_load(acquire)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceRelease(addr unsafe.Pointer) &lt;span style="color:#008000"&gt;// ≈ C11 atomic_store(release)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; RaceReleaseMerge(addr unsafe.Pointer) &lt;span style="color:#008000"&gt;// ≈ C11 atomic_exchange(release)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;RaceRead&lt;/code&gt;/&lt;code&gt;RaceWrite&lt;/code&gt; 是访问点，运行时里 channel 收发、&lt;code&gt;sync.Mutex&lt;/code&gt; 的加解锁、&lt;code&gt;sync/atomic&lt;/code&gt;
的每个原子操作、goroutine 的创建与退出，则在内部调用 &lt;code&gt;raceacquire&lt;/code&gt;/&lt;code&gt;racerelease&lt;/code&gt; 这对原语
把 happens-before 边「焊」上去。换句话说，&lt;a href="../../../part3concurrency/ch11sync/mem"&gt;11.9&lt;/a&gt; 里
规定的每一条建立次序的语义（channel 发送 happens-before 对应接收完成、解锁 happens-before
后续加锁、原子写以 release 顺序同步原子读的 acquire），在 TSan 这里都落成一条具体的图上的边。
内存模型那句抽象的「无 happens-before 即竞争」，于是变成了一个能实际抓现行的判定。&lt;/p&gt;</description></item><item><title>16.3 性能追踪</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/trace/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/trace/</guid><description>&lt;h1 id="163-性能追踪"&gt;16.3 性能追踪&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;pprof&lt;/code&gt;（&lt;a href=".././perf"&gt;16.5&lt;/a&gt;）回答的是「时间花在哪些函数上」。它把一段时间里的 CPU 采样聚合
成一棵调用树，告诉你 &lt;code&gt;json.Marshal&lt;/code&gt; 占了 30% 的 CPU。但它答不了另一类问题：一个请求耗了
50ms，其中 45ms 这个 goroutine &lt;strong&gt;根本没在跑&lt;/strong&gt;，它在等。等什么？等锁？等网络？被 GC 打断？
还是想跑却没有 P 可用（&lt;a href="../../../part3concurrency/ch09sched/steal"&gt;9.2&lt;/a&gt;）？CPU 画像对「等待」
一无所知，因为它只采样正在执行的栈，一个阻塞的 goroutine 不在任何 CPU 上，自然不会被采到。&lt;/p&gt;
&lt;p&gt;回答「为什么等」要靠另一件工具，&lt;strong&gt;执行追踪器&lt;/strong&gt;（execution tracer）。它不做统计抽样，而是
&lt;strong&gt;逐条记录运行时事件&lt;/strong&gt;，按纳秒级时间戳排成一条时间线：哪个 goroutine 在哪一刻被创建、开始
跑、阻塞、被唤醒、结束;每个 P 何时开始与停止调度;每次 GC 走到哪个阶段;每个系统调用何时
进出。把这条时间线交给 &lt;code&gt;go tool trace&lt;/code&gt; 渲染，你能逐毫秒地看清程序里发生了什么。这一节讲它
记录什么、怎么用、以及它近年从「重而全」走向「轻而按需」的演进。&lt;/p&gt;
&lt;h2 id="1631-追踪器记录什么运行时事件的时间线"&gt;16.3.1 追踪器记录什么：运行时事件的时间线&lt;/h2&gt;
&lt;p&gt;执行追踪器内建在运行时里（&lt;code&gt;runtime/trace.go&lt;/code&gt;），它捕获的不是用户代码的函数调用，而是
&lt;strong&gt;运行时与调度器层面的状态转移&lt;/strong&gt;。&lt;code&gt;runtime/trace.go&lt;/code&gt; 的设计注释把这份事件清单列得很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;goroutine 生命周期&lt;/strong&gt;：创建（go 语句）、开始运行、阻塞、被唤醒、退出;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻塞的原因&lt;/strong&gt;：在 channel 收发、在 &lt;code&gt;sync.Mutex&lt;/code&gt;、在网络 I/O（netpoller，&lt;a href="../../../part3concurrency/ch09sched/poll"&gt;9.7&lt;/a&gt;）、在 &lt;code&gt;select&lt;/code&gt; 上各是不同的事件类型;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个 P 的调度活动&lt;/strong&gt;：P 何时启动、停止、被抢占;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GC 相关事件&lt;/strong&gt;：标记开始、标记结束、各 STW（stop-the-world）停顿、堆大小的变化（&lt;a href="../../../part4memory/ch13gc/pacing"&gt;13.3&lt;/a&gt;）;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用&lt;/strong&gt;：进入、返回、以及因系统调用阻塞而交还 P 的时刻。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大多数事件都附带一个&lt;strong&gt;纳秒精度的时间戳&lt;/strong&gt;和一份&lt;strong&gt;栈回溯&lt;/strong&gt;。这意味着 trace 不仅告诉你「这里
发生了一次阻塞」，还告诉你「是哪一行代码、在哪个调用栈深处阻塞的」。把这些事件按 P、按
goroutine 在时间轴上铺开，&lt;code&gt;go tool trace&lt;/code&gt; 画出一张可交互的时间线，你能直接读出：这一刻有
几个 P 在干活、哪个 goroutine 跑在哪个 P 上、它为什么停下、GC 是不是正在和业务抢 CPU。&lt;/p&gt;</description></item><item><title>16.4 代码测试</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/testing/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/testing/</guid><description>&lt;h1 id="164-代码测试"&gt;16.4 代码测试&lt;/h1&gt;
&lt;p&gt;测试在 Go 里不是外挂，而是语言工具链的一等公民。&lt;code&gt;go test&lt;/code&gt; 内建，&lt;code&gt;testing&lt;/code&gt; 包标准，约定
取代配置。这套设计上的选择，深刻塑造了 Go 的工程文化：在别的语言里「要不要写测试、用哪个
框架」是一道需要权衡的决策，在 Go 里它是默认动作。这一节讲清这套机制如何运转、为何如此设计，
以及它从单元测试一路长到 Go 1.18 模糊测试的谱系。&lt;/p&gt;
&lt;h2 id="1641-一等公民约定取代配置"&gt;16.4.1 一等公民：约定取代配置&lt;/h2&gt;
&lt;p&gt;Go 的测试靠约定运转，几乎零配置。三条规则就是全部：测试文件以 &lt;code&gt;_test.go&lt;/code&gt; 结尾，测试函数
形如 &lt;code&gt;func TestXxx(t *testing.T)&lt;/code&gt;，与被测代码放在同一个包里。&lt;code&gt;go test&lt;/code&gt; 会自动发现并运行它们，
不需要 XML 配置、不需要外部 runner、不需要注解。一个项目无论多大，&lt;code&gt;go test ./...&lt;/code&gt; 一行就跑遍
全部测试：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// strings_test.go，与被测包 strings 同目录、同包&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;package&lt;/span&gt; strings
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;import&lt;/span&gt; &lt;span style="color:#a31515"&gt;&amp;#34;testing&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; TestIndex(t *testing.T) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; got := Index(&lt;span style="color:#a31515"&gt;&amp;#34;chicken&amp;#34;&lt;/span&gt;, &lt;span style="color:#a31515"&gt;&amp;#34;ken&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; got != 4 {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; t.Errorf(&lt;span style="color:#a31515"&gt;&amp;#34;Index = %d, want 4&amp;#34;&lt;/span&gt;, got) &lt;span style="color:#008000"&gt;// 报告失败，但不中断&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;「约定优于配置」带来两层收益。其一是零门槛：写测试不需要先学一套框架，函数签名对了就能跑。
其二，也是更深远的一层，是整个生态的统一。所有 Go 项目的测试方式完全一致，于是 CI、覆盖率
工具、IDE、&lt;code&gt;go vet&lt;/code&gt; 都只需面对一种约定，无需为各色框架各写一套适配。这与别家形成鲜明对照：
Java 世界有 JUnit 4 / JUnit 5 / TestNG 之分，注解与 runner 各不相同；Python 有 &lt;code&gt;unittest&lt;/code&gt;、
&lt;code&gt;pytest&lt;/code&gt;、&lt;code&gt;nose&lt;/code&gt; 并存，发现规则与 fixture 机制互不兼容。框架的多样性把「跑某项目的测试」变成
一件需要先读文档的事。Go 用一套内建约定把这件事抹平到了零。&lt;/p&gt;</description></item><item><title>16.5 基准测试与性能画像</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/perf/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/perf/</guid><description>&lt;h1 id="165-基准测试与性能画像"&gt;16.5 基准测试与性能画像&lt;/h1&gt;
&lt;p&gt;优化的第一原则是先测量，再动手。凭直觉猜瓶颈往往猜错，真正的热点常常不在你以为的地方。Go 把
这套纪律所需的工具全部做进了工具链：基准测试（benchmark）回答「多快」，性能画像（profiling）
回答「慢在哪、内存花在哪」，而 &lt;code&gt;benchstat&lt;/code&gt; 用统计方法回答「这次改动到底是真的快了，还是只是
噪声」。这一节把这三件工具讲透，并把它们串成一条「画像找热点 → 改 → 基准验证」的测量驱动闭环,
这条闭环再往上，便与 PGO（&lt;a href="../../ch15compile/optimize"&gt;15.3&lt;/a&gt;）和执行追踪
（&lt;a href=".././trace"&gt;16.3&lt;/a&gt;）合流，构成 Go 完整的性能工程方法论。&lt;/p&gt;
&lt;h2 id="1651-基准测试让多快可复现"&gt;16.5.1 基准测试：让「多快」可复现&lt;/h2&gt;
&lt;p&gt;基准测试用 &lt;code&gt;func BenchmarkXxx(b *testing.B)&lt;/code&gt; 写，&lt;code&gt;go test -bench&lt;/code&gt; 运行。它要解决的核心难题是
&lt;strong&gt;测量精度&lt;/strong&gt;：单次调用一个纳秒级函数，时钟分辨率与调用开销会把信号淹没。Go 的办法是把待测代码
放进循环跑很多次，再除以次数得到「每次操作的耗时」（ns/op）。但循环要跑多少次？跑少了不稳定，
跑多了浪费时间。早期写法是手写一个从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;b.N&lt;/code&gt; 的循环，框架&lt;strong&gt;自动反复调整 &lt;code&gt;b.N&lt;/code&gt;、多跑几轮&lt;/strong&gt;，
直到测量窗口足够长、结果足够稳定为止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; BenchmarkFib(b *testing.B) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;for&lt;/span&gt; i := 0; i &amp;lt; b.N; i++ { &lt;span style="color:#008000"&gt;// 框架反复加大 b.N 直到耗时稳定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fib(30)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这套 &lt;code&gt;b.N&lt;/code&gt; 机制朴素可靠，却有两处长期为人诟病的坑。其一是&lt;strong&gt;计时边界&lt;/strong&gt;：若基准函数里有昂贵的
准备工作，它会被算进每次测量，得手动 &lt;code&gt;b.ResetTimer()&lt;/code&gt;/&lt;code&gt;b.StopTimer()&lt;/code&gt; 把它摘出去（见
&lt;a href="#1653-%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95%E7%9A%84%E4%B8%A4%E4%B8%AA%E7%BB%8F%E5%85%B8%E9%99%B7%E9%98%B1"&gt;16.5.3&lt;/a&gt;）。其二是&lt;strong&gt;死代码消除&lt;/strong&gt;：编译器看到 &lt;code&gt;fib(30)&lt;/code&gt; 的返回值
没人用，可能直接把整个调用优化掉，于是你测了个寂寞。&lt;/p&gt;</description></item><item><title>16.6 运行时统计量</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/metric/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/metric/</guid><description>&lt;h1 id="166-运行时统计量"&gt;16.6 运行时统计量&lt;/h1&gt;
&lt;p&gt;画像（&lt;a href=".././perf"&gt;16.5&lt;/a&gt;）与追踪（&lt;a href=".././trace"&gt;16.3&lt;/a&gt;）属于「出事时拉来诊断」的工具：它们成本
偏高、产出庞大，适合在你已经怀疑某处有问题时，针对一段时间或一次请求采下来细看。但生产环境
里更常见的问题不是「现在帮我剖析一下」，而是「这个服务过去一周健康吗、什么时候开始劣化的、
该不该半夜把人叫起来」。回答这类问题靠的不是一次性的深剖，而是&lt;strong&gt;持续的、低成本的、可长期
保留的数值&lt;/strong&gt;：堆有多大、GC 多频繁、goroutine 数量是否在涨、调度延迟的尾部有没有抬头。这类
数值就是&lt;strong&gt;运行时指标&lt;/strong&gt;（metrics）。&lt;/p&gt;
&lt;p&gt;指标和画像的区别，本质是「聚合 vs 明细」与「常驻 vs 按需」。画像把每一次分配、每一段 CPU
归因到具体调用栈，信息量大、采集贵；指标只保留聚合后的标量或分布，单次读取近乎免费，因而
可以每隔几秒采一次、连续采上几个月。这一节讲清 Go 暴露指标的两套接口、它们的演进，以及
指标如何接入完整的可观测性体系。&lt;/p&gt;
&lt;h2 id="1661-从-memstats-到-runtimemetrics"&gt;16.6.1 从 MemStats 到 runtime/metrics&lt;/h2&gt;
&lt;p&gt;历史上，程序读运行时内存指标的唯一入口是 &lt;code&gt;runtime.ReadMemStats&lt;/code&gt;，它填充一个 &lt;code&gt;MemStats&lt;/code&gt;
结构（&lt;a href="../../../part4memory/ch12alloc/mstats"&gt;12.8&lt;/a&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// runtime.MemStats：字段写死在结构体里（节选）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; MemStats &lt;span style="color:#00f"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Alloc &lt;span style="color:#2b91af"&gt;uint64&lt;/span&gt; &lt;span style="color:#008000"&gt;// 当前存活对象占用的堆字节&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; HeapInuse &lt;span style="color:#2b91af"&gt;uint64&lt;/span&gt; &lt;span style="color:#008000"&gt;// 含已用 span 的堆字节&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NumGC &lt;span style="color:#2b91af"&gt;uint32&lt;/span&gt; &lt;span style="color:#008000"&gt;// 已完成的 GC 轮数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PauseTotalNs &lt;span style="color:#2b91af"&gt;uint64&lt;/span&gt; &lt;span style="color:#008000"&gt;// 累计 STW 停顿（纳秒）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PauseNs [256]&lt;span style="color:#2b91af"&gt;uint64&lt;/span&gt; &lt;span style="color:#008000"&gt;// 最近 256 次停顿的环形缓冲&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// ... 还有约三十个字段&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;var&lt;/span&gt; m runtime.MemStats
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;runtime.ReadMemStats(&amp;amp;m) &lt;span style="color:#008000"&gt;// 读取时需短暂 stop-the-world&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这套接口有三处硬伤。其一，字段&lt;strong&gt;写死&lt;/strong&gt;在结构体里：运行时想暴露一个新指标，就得往
&lt;code&gt;MemStats&lt;/code&gt; 里加字段、改公开 API，受 Go 兼容性承诺约束，加得很慎重，于是许多内部状态根本
没有出口。其二，&lt;code&gt;ReadMemStats&lt;/code&gt; 为了取得自洽快照，读取时要短暂 &lt;strong&gt;stop-the-world&lt;/strong&gt;，在高频
采集下成本不可忽略。其三，停顿信息只有一个 &lt;code&gt;PauseNs&lt;/code&gt; 环形数组和累计值，想知道「停顿的 P99
是多少」得自己从原始数组里算，而那个数组只存最近 256 次，早就漏掉了长期分布。&lt;/p&gt;</description></item><item><title>16.7 语言服务协议</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/gopls/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch16tools/gopls/</guid><description>&lt;h1 id="167-语言服务协议"&gt;16.7 语言服务协议&lt;/h1&gt;
&lt;p&gt;自动补全、跳转定义、实时报错、重构,现代编辑器里这些功能，在 Go 这边由 &lt;strong&gt;&lt;code&gt;gopls&lt;/code&gt;&lt;/strong&gt;（Go language
server，读作「go please」）提供。它背后是&lt;strong&gt;语言服务器协议&lt;/strong&gt;（Language Server Protocol，LSP）。
本节讲清三件事：LSP 解开的那个组合爆炸结、gopls 如何建立在编译器前端的可复用库之上、以及它
为何称得上 Go 工具链思想的集大成者。&lt;/p&gt;
&lt;h2 id="1671-lsp解开-mn-的结"&gt;16.7.1 LSP：解开 M×N 的结&lt;/h2&gt;
&lt;p&gt;设想没有 LSP 的世界。要让 &lt;strong&gt;M 个编辑器&lt;/strong&gt;都支持 &lt;strong&gt;N 种语言&lt;/strong&gt;的智能功能，朴素的做法是为每一对
「编辑器 × 语言」单独写一套集成：VS Code 要懂 Go、懂 Rust、懂 Python，Vim 也要各懂一遍,补全、
跳转、报错的逻辑在每一对里都重写。代价是 &lt;strong&gt;M×N&lt;/strong&gt; 套实现，且任何一门语言的语义更新，都要在
M 个编辑器里各自跟进。这条增长曲线注定走不远。&lt;/p&gt;
&lt;p&gt;LSP 把它解成 &lt;strong&gt;M+N&lt;/strong&gt;。微软在 2016 年 6 月为 Visual Studio Code 提出这套协议(后与 Red Hat、
Codenvy 合作开放，现已成行业标准)。它的关键是定义&lt;strong&gt;一套与语言、与编辑器都无关的标准协议&lt;/strong&gt;：
每门语言只需写&lt;strong&gt;一个&lt;/strong&gt;语言服务器（实现协议），每个编辑器只需写&lt;strong&gt;一个&lt;/strong&gt; LSP 客户端,此后任意
编辑器配任意语言，自动打通。语言服务器是一个独立进程，编辑器通过标准输入输出或套接字与它通信。&lt;/p&gt;
&lt;p&gt;协议本身建立在 &lt;strong&gt;JSON-RPC&lt;/strong&gt; 之上,只有三类消息：客户端发&lt;strong&gt;请求&lt;/strong&gt;（request）等服务器回&lt;strong&gt;响应&lt;/strong&gt;
（response），以及单向的&lt;strong&gt;通知&lt;/strong&gt;（notification）。位置用「文档 URI + 行列号」表示，刻意避开任何
语言特有的抽象（如某语言的 AST 节点），这正是它能跨语言通用的前提。一次「跳转到定义」的往返
是这样：&lt;/p&gt;</description></item></channel></rss>