Gin + pprof 性能分析演示项目
这是一个完整的 Gin 框架集成 net/http/pprof 的演示项目,包含各种性能问题场景和详细的分析指南。
完整的示例代码放在最后。
🚀 快速开始
1. 启动服务
bash
# 直接运行
go run main.go
# 或编译后运行
go build -o app main.go
./app
2. 访问服务
打开浏览器访问: http://localhost:8080
你会看到一个交互式主页,包含所有演示接口的说明和使用方法。
3. 查看 pprof 端点
访问: http://localhost:8080/debug/pprof/
📚 项目结构
gogogo/
├── main.go # 主程序,包含 Gin + pprof 集成
├── go.mod # Go 模块定义
├── go.sum # 依赖锁定文件
├── README.md # 本文件
├── PPROF_GUIDE.md # 详细的 pprof 使用指南
└── profiles/ # 采集的 profile 数据(运行后自动创建)
🎯 演示接口
| 接口 | 说明 | 用途 |
|---|---|---|
GET / |
主页 | 查看所有演示说明 |
GET /cpu-demo |
CPU 密集型任务 | CPU 性能分析演示 |
GET /memory-leak-demo |
内存泄漏演示 ⚠️ | 内存泄漏分析演示 |
GET /goroutine-leak-demo |
goroutine 泄漏演示 ⚠️ | goroutine 泄漏分析演示 |
GET /lock-contention-demo |
锁竞争演示 | 锁竞争分析演示 |
GET /heap-allocation-demo |
堆内存分配演示 | 堆内存分析演示 |
GET /stats |
运行时统计 | 查看当前运行状态 |
🔧 pprof 端点
| 端点 | 说明 |
|---|---|
/debug/pprof/ |
pprof 首页 |
/debug/pprof/profile |
CPU 性能分析 |
/debug/pprof/heap |
堆内存分析 |
/debug/pprof/goroutine |
Goroutine 状态 |
/debug/pprof/mutex |
锁竞争分析 |
/debug/pprof/block |
阻塞操作分析 |
/debug/pprof/allocs |
内存分配统计 |
/debug/pprof/trace |
执行跟踪 |
触发 和 采集
sh
# 触发
curl http://localhost:8080/memory-leak-demo
#采集
curl -o heap1.prof "http://localhost:8080/debug/pprof/heap?gc=1"
一、CPU 性能分析
快照: 30 秒内的所有 CPU 活动
1. Top 命令解读
bash
pprof> top # 默认按 flat 排序
pprof> top -cum # 按 cum 排序
pprof> top10 # 显示前 10 个
1.1 基本输出示例
执行 go tool pprof cpu.prof 后输入 top 会看到:
pprof> top
Showing nodes accounting for 100%, 100ms of 100ms total
flat flat% sum% cum cum%
50ms 50.00% 50.00% 100ms 100.00% main.cpuIntensiveTask
30ms 30.00% 30.00% 30ms 30.00% runtime.memmove
20ms 20.00% 50.00% 20ms 20.00% runtime.duffzero
0 0% 50.00% 100ms 100.00% main.main.func1
0 0% 50.00% 100ms 100.00% github.com/gin-gonic/gin.(*Context).JSON
1.2 关键指标详解
flat (Flat 时间)
- 含义 : 函数自身执行的 CPU 时间
- 不包括: 调用的子函数的时间
- 特点: 这个函数自己跑花了多少时间
- 例子 :
50ms表示main.cpuIntensiveTask函数自己执行用了 50ms
简单理解: 这就是函数"自己干的时间"
flat% (Flat 百分比)
- 含义: flat 时间占总采样时间的百分比
- 例子 :
50.00%表示这个函数占用了总 CPU 时间的 50%
sum% (累积百分比)
- 含义: 从上到下累积的百分比
- 例子 : 第一行
50%,第二行50%表示前两个函数加起来占用了 100% - 用途: 快速知道前 N 个函数占用了多少 CPU
cum (Cumulative 时间)
- 含义 : 函数及其调用的所有子函数的总 CPU 时间
- 包括: 子函数的执行时间
- 例子 :
100ms表示main.cpuIntensiveTask+ 它调用的所有子函数总共用了 100ms
简单理解: 这是函数"全家"的时间(自己 + 所有子孙)
cum% (Cumulative 百分比)
- 含义: cum 时间占总采样时间的百分比
1.3 关键对比: flat vs cum
这是 CPU profile 中最重要的概念!
函数 A {
函数 B() // 耗时 30ms
自己执行 // 耗时 50ms
函数 C() // 耗时 20ms
}
函数 A 的指标:
- flat = 50ms (函数 A 自己执行的时间)
- cum = 100ms (50ms + 30ms + 20ms = 100ms)
通俗解释:
- flat: 就是你在这个函数里的时间,不包括你调用的其他函数
- cum: 包括你调用的所有函数加起来的时间
2. 其他重要说明
bash
# 1. 触发 CPU 密集型任务
for i in {1..20}; do
curl http://localhost:8080/cpu-demo
echo "触发第 $i 次"
sleep 1
done
# 2. 采集 CPU profile (30秒) (执行完 第1步, 立即执行 该步骤)
curl -o cpu.prof "http://localhost:8080/debug/pprof/profile?seconds=30"
# 3. 分析
go tool pprof cpu.prof
# 4. 可视化
go tool pprof -http=:8081 cpu.prof
# 浏览器访问 http://localhost:8081
说明:
- 分析 和 4. 可视化 这两种方式看到的数据是完全一样的,只是呈现方式不同。无论哪种方式,数据都来自同一个 cpu.prof 文件。
- 交互式命令行 go tool pprof cpu.prof
适合快速查看 和 简单分析 - Web 可视化 go tool pprof -http=:8081 cpu.prof
适合深度分析 和 可视化, 提供多种视图
注意:
sh
# 方式1: 命令行(需要安装 Graphviz(dot 命令) 才能用 web 命令)
go tool pprof cpu.prof
pprof> web # 这个需要 Graphviz (choco install graphviz、brew install graphviz、apt install graphviz、yum install graphviz)
# 方式2: Web 可视化(不需要额外软件)
go tool pprof -http=:8081 cpu.prof
# 浏览器打开 http://localhost:8081
问题: 命令行 web 打开后 和 Web -http 打开后 的 页面内容 是完全一样的吗?
答: 不是完全一样的!
- pprof> web: 只是一个静态的调用图 SVG 图片,没有交互功能。
- -http=:8081: 是一个功能完整的 Web 应用,包含多种视图和交互功能
推荐使用 Web 方式 (-http=:8081),功能更强大,体验更好! 🚀
提要:
- 为什么有些函数 flat = 0?
因为 这个函数 自己没有执行代码,只是调用了其他函数。 - 优化的重点:
sh
1. 你的业务代码 (main.*) ← 优化重点
2. 标准库 (net, os, time) ← 了解即可
3. runtime/* ← 了解即可
4. syscall/* ← 忽略
5. internal/* ← 忽略
二、内存分析
快照式: 记录当前时刻的内存分配状态
关键要点:内存泄漏 和 分配热点
四个 内存分析的 核心指标
sh
inuse vs alloc
↓ ↓
当前使用的 所有分配的(包括已释放的)
space vs objects
↓ ↓
大小 对象数量
-
inuse_space (
最常用, 90% 的情况下,只需inuse_space! 🎯)含义: 当前还在使用的
内存大小, 没有被 GC 回收的内存通俗解释:
就像你钱包里的钱:
- 你现在还有多少钱 = inuse_space
- 不管你花掉了多少,只看你现在还剩多少
-
inuse_objects
含义:当前还在使用的
对象数量, 没有被 GC 回收的对象个数通俗解释:
就像你房间里的物品:
- 你现在还有多少件物品 = inuse_objects
- 不管你扔掉了多少,只看你现在还剩多少
-
alloc_space
含义:程序运行以来分配的
总内存大小, 包括已经被 GC 回收的内存。alloc_space 的时间基准是: 自程序启动以来 的 累计值
通俗解释:
就像你总共赚了多少钱:
- 不管你花了多少,只看你总共赚了多少
- 即使钱花光了,你赚的钱也算在内
- 同理
内存大小 和 对象数量
场景举例
sh
场景 A: 1 个 100MB 的切片 → inuse_objects=1, inuse_space=100MB
场景 B: 1000000 个 100 字节的切片 → inuse_objects=1000000, inuse_space=100MB
虽然总内存相同,但分配代价差异巨大:
- 场景 A: 分配 1 次
- 场景 B: 分配 100 万次,需要大量 malloc/free 操作,GC 压力大
inuse_objects 和 alloc_objects
本质上说:
- inuse_objects 是 当前 堆内中 存活 的 对象数量
- alloc_objects 是 内存分配 的 次数
举例:
go
func main() {
// 1. 初始分配
arr := make([]int, 0, 5) // 第1次分配:容量5
// 2. 添加元素
for i := 0; i < 10; i++ {
arr = append(arr, i)
// i=0,1,2,3,4: 容量5够用,无扩容
// i=5: 容量5→8,第2次分配(创建容量8的新数组)
// i=6,7: 容量8够用,无扩容
// i=8: 容量8→16,第3次分配(创建容量16的新数组)
// i=9: 容量16够用,无扩容
}
}
alloc_objects = 3(容量5、8、16,共分配了3个数组对象的内存)
inuse_objects = 1(只有容量16的数组在 堆上 存活,前两个已回收)
场景1: 发现内存增长(对比法 - 推荐)
需要对比: 至少采集 两次 才能发现问题
bash
# 1. 采集基线 (初始状态)
curl -o heap1.prof "http://localhost:8080/debug/pprof/heap?gc=1"
echo "基线采集完成"
# 2. 触发内存泄漏(多次调用)
for i in {1..20}; do
curl http://localhost:8080/memory-leak-demo
echo "触发第 $i 次"
sleep 1
done
# 3. 采集当前状态
curl -o heap2.prof "http://localhost:8080/debug/pprof/heap?gc=1"
echo "对比采集完成"
# 4. 对比分析 (找出增长部分)
go tool pprof -base=heap1.prof heap2.prof
# 5. 查看增长最多的函数
pprof> top
# 6. 可视化对比
go tool pprof -http=:8082 -base=heap1.prof heap2.prof
分析
输入 内存分析 两文件比较命令
sh
go tool pprof -base=.\heap1.prof .\heap2.prof
sh
File: gogogos.exe
Build ID: C:\Users\whero\AppData\Local\go-build\d4\d4e1ea12f7b62fd37f3a9b8c4bf4ddba798b703b0ddc64c326dc7c5fac76e2dc-d\gogogos.exe2026-03-19 20:29:36.191102 +0800 CST
Type: inuse_space // 默认查看的是:当前使用 的 内存大小
Time: 2026-03-27 14:08:03 CST
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10 // 输入 交互命令 查看前50个
// 显示的节点共计 16241.09kB,占 16241.09kB 总量的 100%。
// 言外之意,两个文件 总增长内存: 16241.09kB (约 15.86MB) 这个量为 100% 的对应内存指标。
// 这个 百分之百 的对应量 可以是 差值(类似:时间段) 可以是 总值(类似:时间点),
// 可以是 内存,也可以是 cpu资源
Showing nodes accounting for 16241.09kB, 100% of 16241.09kB total
flat flat% sum% cum cum% // 显示 结果 列表
9216.70kB 56.75% 56.75% 9728.80kB 59.90% fmt.Sprintf
3591kB 22.11% 78.86% 3591kB 22.11% runtime.allocm
1892.02kB 11.65% 90.51% 11620.83kB 71.55% main.memoryLeakTask
1029.26kB 6.34% 96.85% 1029.26kB 6.34% vendor/golang.org/x/net/http2/hpack.init
512.10kB 3.15% 100% 512.10kB 3.15% fmt.init.func1
0 0% 100% 512.10kB 3.15% fmt.newPrinter
0 0% 100% 11620.83kB 71.55% github.com/gin-gonic/gin.(*Context).Next
0 0% 100% 11620.83kB 71.55% github.com/gin-gonic/gin.(*Engine).ServeHTTP
0 0% 100% 11620.83kB 71.55% github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
0 0% 100% 11620.83kB 71.55% github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
解读:
一眼就能看出 fmt.Sprintf - 最大内存泄漏点!
flat = 9216.70kB (约 9MB): fmt.Sprintf 本身分配了 9MB 内存
flat% = 56.75%: 占总增长内存的 56.75%,超过一半!
cum = 9728.80kB: 包括子函数共约 9.7MB
超过一半的内存增长都来自这里,是需要立即优化的点!
fmt.Sprintf 是标准函数,找到 调用 该函数 且造成 泄露的 又是我们自己写的函数 就成了 重中之重了。
从上图中 可以 看出 main.memoryLeakTask 的cum% 的指标 有 调用 fmt.Sprintf 的嫌疑。
我们可以 使用 web 交互命令 进一步 确认, 也可以 通过 list main.memoryLeakTask 交互命令 直接确定。
然后就是 针对特别情况的 特别优化手段。
场景2: 找出内存分配热点
sh
# 1. 触发任务(单次或多次)
curl http://localhost:8080/memory-leak-demo
# 2. 立即采集(捕获任务后的状态)
curl -o heap.prof "http://localhost:8080/debug/pprof/heap?gc=1"
# 3. 分析内存分配
go tool pprof heap.prof
pprof> top # 查看内存占用
pprof> top -alloc_objects # 查看分配对象数
pprof> list main.memoryLeakTask # 查看具体分配点
场景3: 找出所有分配(包括已释放)
sh
# 1. 重启服务(清空历史分配数据)
# Ctrl+C 停止服务,然后重新启动 go run main.go
# 2. 触发任务
for i in {1..10}; do
curl http://localhost:8080/heap-allocation-demo
done
# 3. 采集分配历史
curl -o heap.prof "http://localhost:8080/debug/pprof/heap"
# 4. 查看所有分配(包括已释放的)
go tool pprof heap.prof
pprof> sample_index=alloc_space # 切换到分配空间指标
pprof> top
# 或者直接使用参数
go tool pprof -sample_index=alloc_space heap.prof
pprof> top
说明:
- gc 参数
sh
# 不执行 GC (默认)
curl "http://localhost:8080/debug/pprof/heap"
# 采集前执行 GC (推荐)
curl "http://localhost:8080/debug/pprof/heap?gc=1"
gc=1: 采集前强制执行一次 GC,清除已释放的内存
推荐: 使用 gc=1 可以更准确地找出真正的内存泄漏
- 内存分配热点:
程序中 频繁进行内存分配 的 代码位置 或 函数
三、Goroutine 分析
快照式: 记录采集时刻所有 goroutine 的堆栈信息。
说明: func1 表示 第一个 匿名函数
相同堆栈(核心概念)
1.1 堆栈的概念
比较迷糊人的是,看到 堆栈 总以为是 堆地址 和 栈地址 类的信息, 其实完全不是。
堆栈 是 goroutine 的函数调用链,记录了 goroutine 从启动到当前执行位置经过的所有函数。
所以简单的说, 就是 函数调用栈。
示例:
go
func A() {
B()
}
func B() {
C()
}
func C() {
time.Sleep(1 * time.Second) // ← goroutine 停在这里
}
这个 goroutine 的堆栈是:
C ← 当前执行位置
B ← 调用 C
A ← 调用 B
main ← main 调用 A
1.2 "相同堆栈"的含义
相同堆栈 = 多个 goroutine 的调用链完全一致
示例:
Goroutine 1: main → A → B → C → time.Sleep
Goroutine 2: main → A → B → C → time.Sleep
Goroutine 3: main → A → B → C → time.Sleep
这三个 goroutine 的堆栈完全相同 ,traces 命令会 合并显示 (这也是 traces命令强大 且 实用 的 地方):
-----------+-------------------------------------------------------
3 runtime.gopark
time.Sleep
C
B
A
main
-----------+-------------------------------------------------------
重点说明:
上面所展现的,就是 traces 命令 列举数据的 格式。
- 左边数字:该堆栈有多少个 goroutine
- 右边堆栈:从底向上调用
traces 命令的作用:
从上 也能推断出 traces 命令的 作用
-
查看每个 goroutine 的完整调用链
-
识别相同堆栈的 goroutine 数量
-
定位 goroutine 停在哪个函数
-
区分不同类型的 goroutine(正常 vs 异常)
-----------+-------------------------------------------------------
3 runtime.gopark ← 运行时挂起
time.Sleep ← 阻塞原因(从 函数调用链 可以看出)
C ← 业务代码
B ← 业务代码
A ← 业务代码
main
-----------+-------------------------------------------------------
1.3 对比:不同堆栈
不同堆栈 = 调用链有差异
示例:
Goroutine 1: main → A → B → C → time.Sleep
Goroutine 2: main → X → Y → Z → select {}
Goroutine 3: main → processRequest → runtime.netpollblock
traces 命令会分别显示:
-----------+-------------------------------------------------------
1 runtime.gopark
time.Sleep
C
B
A
main
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.selectgo
Z
Y
X
main
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
processRequest
main
-----------+-------------------------------------------------------
1.4 相同堆栈的意义
相同堆栈意味着:
- 相同的执行路径:这些 goroutine 经过了相同的函数调用
- 相同的阻塞位置:都停在同一个函数的同一行
- 相同的创建原因:通常是由同一个地方创建的
- 相同的业务含义:都在做同一件事
1.5 为什么 相同堆栈 要绝对重视
1.5.1 识别模式
相同堆栈 = 模式识别
| 相同堆栈数量 | 可能的原因 | 严重性 |
|---|---|---|
| 1-2 | 正常的偶然情况 | ✓ 无问题 |
| 3-10 | 可能的正常并发 | ⚠️ 需观察 |
| 10-100 | 可能的泄漏或高并发 | 🚨 需检查 |
| 100+ | 严重泄漏或设计问题 | 🔴 必须修复 |
1.5.2 区分正常和异常
正常的相同堆栈:
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
net/http.(*Server).Serve
main.main
-----------+-------------------------------------------------------
✓ 只有 1 个,是 HTTP 服务主 goroutine,正常
异常的相同堆栈:
-----------+-------------------------------------------------------
200 runtime.gopark
runtime.block
main.goroutineLeakTask.func1
-----------+-------------------------------------------------------
✗ 200 个,都在业务代码中阻塞,严重泄漏
1.6 相同堆栈的常见场景
- 场景 1:Worker Pool(正常)
go
func worker(id int) {
for {
task := <-taskChan // ← 等待任务
process(task)
}
}
func main() {
for i := 0; i < 10; i++ {
go worker(i) // 创建 10 个 worker
}
}
traces 输出:
-----------+-------------------------------------------------------
10 runtime.gopark
runtime.chanrecv
worker
main.main
-----------+-------------------------------------------------------
分析:
- ✓ 10 个 goroutine 相同堆栈
- ✓ 但数量固定(10 个)
- ✓ 这是正常的 Worker Pool 模式
- 场景 2:goroutine 泄漏(异常)
go
func main() {
for i := 0; i < 100; i++ {
go func() {
time.Sleep(10 * time.Minute) // ← sleep 10 分钟
}()
}
}
traces 输出:
-----------+-------------------------------------------------------
100 runtime.gopark
time.Sleep
main.main.func1
main.main
-----------+-------------------------------------------------------
分析:
- ✗ 100 个 goroutine 相同堆栈
- ✗ 持续增长(每次循环都创建)
- ✗ 这是严重的 goroutine 泄漏
- 场景 3:HTTP 并发请求(正常)
go
func main() {
for i := 0; i < 50; i++ {
go func() {
resp, _ := http.Get("https://api.example.com/data")
process(resp)
}()
}
}
traces 输出:
-----------+-------------------------------------------------------
50 runtime.gopark
runtime.netpollblock
net/http.(*Transport).RoundTrip
net/http.Get
main.main.func1
main.main
-----------+-------------------------------------------------------
分析:
- ✓ 50 个 goroutine 相同堆栈
- ✓ 但都在等待网络 I/O(正常操作)
- ✓ 这是正常的并发 HTTP 请求
1.7 相同堆栈 判断是否正常
1.7.1 判断标准
| 判断维度 | 正常 | 异常 |
|---|---|---|
| 数量 | 固定(如 10 个 worker) | 持续增长 |
| 堆栈内容 | 系统函数(runtime.*, net/http.*) |
业务代码(main.*) |
| 阻塞类型 | netpollblock(网络等待) |
block(永久阻塞) |
| 创建频率 | 启动时创建一次 | 持续创建 |
1.7.2 判断流程
看到相同堆栈
↓
数量是否固定?
↓
否 → 可能泄漏 🚨
是 ↓
阻塞类型是什么?
↓
netpollblock(网络)→ 正常 ✓
block(select{}) → 泄漏 🚨
chanrecv(channel) → 需检查 ⚠️
记忆口诀
相同堆栈看数量,
一百以上要警惕。
业务代码在堆栈,
多半就是有问题。
netpollblock 正常,
block 类型要注意。
固定数量是 Worker,
持续增长是泄漏。
常规的 判断标准
有泄漏:
- ✗
flat值大(如 100) - ✗
main.*函数出现在堆栈中 - ✗ 多个 goroutine 堆栈相同
无泄漏:
- ✓
flat值小(如 0-2) - ✓ 没有
main.*函数 - ✓ 只有
runtime.*和net/http.*系统函数
修复方案
- Worker Pool: 固定 goroutine 数量
- Context: 添加超时控制
- Semaphore: 限制并发数
场景: 检测 goroutine 泄漏(对比法 - 推荐)
bash
# 1. 采集基线(正常状态)
curl -o goroutine1.prof "http://localhost:8080/debug/pprof/goroutine"
# 2. 触发 goroutine 泄漏
for i in {1..10}; do
curl -s http://localhost:8080/goroutine-leak-demo > /dev/null
echo "触发第 $i 次"
sleep 2
done
# 3. 采集泄漏状态
curl -o goroutine2.prof "http://localhost:8080/debug/pprof/goroutine"
# 4. 对比分析
go tool pprof -base=goroutine1.prof goroutine2.prof
pprof> top
pprof> traces
查看热点函数 (top 命令)
bash
(pprof) top 50
Showing nodes accounting for 203, 99.51% of 204 total
flat flat% sum% cum cum%
202 99.02% 99.02% 202 99.02% runtime.gopark
1 0.49% 99.51% 1 0.49% runtime.goroutineProfileWithLabels
0 0% 99.51% 1 0.49% bufio.(*Reader).Peek
0 0% 99.51% 1 0.49% bufio.(*Reader).fill
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.(*Context).Next
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.(*Engine).Run
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.(*Engine).ServeHTTP
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
0 0% 99.51% 1 0.49% github.com/gin-gonic/gin.LoggerWithConfig.func1
0 0% 99.51% 1 0.49% internal/poll.(*FD).Accept
0 0% 99.51% 1 0.49% internal/poll.(*FD).Read
0 0% 99.51% 1 0.49% internal/poll.(*FD).acceptOne
0 0% 99.51% 2 0.98% internal/poll.(*pollDesc).wait
0 0% 99.51% 2 0.98% internal/poll.execIO
0 0% 99.51% 2 0.98% internal/poll.runtime_pollWait
0 0% 99.51% 2 0.98% internal/poll.waitIO
0 0% 99.51% 200 98.04% main.goroutineLeakTask.func1
0 0% 99.51% 1 0.49% main.main
0 0% 99.51% 1 0.49% main.setupPPProf.WrapH.func10
0 0% 99.51% 1 0.49% net.(*TCPListener).Accept
0 0% 99.51% 1 0.49% net.(*TCPListener).accept
0 0% 99.51% 1 0.49% net.(*conn).Read
0 0% 99.51% 1 0.49% net.(*netFD).Read
0 0% 99.51% 1 0.49% net.(*netFD).accept
0 0% 99.51% 1 0.49% net/http.(*Server).ListenAndServe
0 0% 99.51% 1 0.49% net/http.(*Server).Serve
0 0% 99.51% 2 0.98% net/http.(*conn).serve
0 0% 99.51% 1 0.49% net/http.(*connReader).Read
0 0% 99.51% 1 0.49% net/http.serverHandler.ServeHTTP
0 0% 99.51% 1 0.49% net/http/pprof.handler.ServeHTTP
0 0% 99.51% 200 98.04% runtime.block
0 0% 99.51% 1 0.49% runtime.main
0 0% 99.51% 2 0.98% runtime.netpollblock
0 0% 99.51% 1 0.49% runtime.pprof_goroutineProfileWithLabels
0 0% 99.51% 1 0.49% runtime/pprof.(*Profile).WriteTo
0 0% 99.51% 1 0.49% runtime/pprof.writeGoroutine
0 0% 99.51% 1 0.49% runtime/pprof.writeRuntimeProfile
查看详细堆栈 (traces 命令)
(pprof) traces
File: gogogos.exe
Build ID: C:\Users\whero\AppData\Local\Temp\go-build3469003199\b001\exe\gogogos.exe2026-03-29 17:18:39.4672414 +0800 CST
Type: goroutine
Time: 2026-03-29 17:24:27 CST
-----------+-------------------------------------------------------
200 runtime.gopark
runtime.block
main.goroutineLeakTask.func1
-----------+-------------------------------------------------------
1 runtime.goroutineProfileWithLabels
runtime.pprof_goroutineProfileWithLabels
runtime/pprof.writeRuntimeProfile
runtime/pprof.writeGoroutine
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
main.setupPPProf.WrapH.func10
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
github.com/gin-gonic/gin.(*Context).Next
runtime/pprof.writeGoroutine
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
main.setupPPProf.WrapH.func10
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
github.com/gin-gonic/gin.(*Context).Next
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
main.setupPPProf.WrapH.func10
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.LoggerWithConfig.func1
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
github.com/gin-gonic/gin.(*Engine).ServeHTTP
github.com/gin-gonic/gin.LoggerWithConfig.func1
github.com/gin-gonic/gin.(*Context).Next
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
github.com/gin-gonic/gin.(*Engine).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
github.com/gin-gonic/gin.(*Engine).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
1 runtime.gopark
runtime.netpollblock
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.waitIO
internal/poll.execIO
internal/poll.(*FD).Read
net.(*netFD).Read
net.(*conn).Read
net/http.(*connReader).Read
bufio.(*Reader).fill
bufio.(*Reader).Peek
net/http.(*conn).serve
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.waitIO
internal/poll.execIO
internal/poll.(*FD).acceptOne
internal/poll.(*FD).Accept
net.(*netFD).accept
net.(*TCPListener).accept
net.(*TCPListener).Accept
net/http.(*Server).Serve
net/http.(*Server).ListenAndServe
github.com/gin-gonic/gin.(*Engine).Run
main.main
runtime.main
-----------+-------------------------------------------------------
第 1 段:200 个泄漏的 goroutine 🚨
-----------+-------------------------------------------------------
200 runtime.gopark
runtime.block
main.goroutineLeakTask.func1
-----------+-------------------------------------------------------
详细分析:
| 层级 | 函数名 | 说明 |
|---|---|---|
| 1 | runtime.gopark |
goroutine 被挂起(等待状态) |
| 2 | runtime.block |
阻塞原因:block 类型 |
| 3 | main.goroutineLeakTask.func1 |
🚨 业务代码 - 泄漏点 |
关键发现:
- ✗ 200 个 goroutine 完全相同的堆栈
- ✗ 堆栈深度只有 3 层,非常简单
- ✗
main.goroutineLeakTask.func1是匿名函数(func1表示第一个匿名函数) - ✗ 阻塞类型:
runtime.block表示被阻塞
对应的源码(main.go 第 40-44 行):
go
go func(n int) {
defer wg.Done()
// 故意不使用 channel 或 done channel
// 这里会永远阻塞
select {} // ← 所有 200 个 goroutine 都停在这里
}(i)
结论 :🚨 严重的 goroutine 泄漏,200 个 goroutine 永远退出不了。
第 2 段:pprof 采集工具创建的 goroutine
-----------+-------------------------------------------------------
1 runtime.goroutineProfileWithLabels
runtime.pprof_goroutineProfileWithLabels
runtime/pprof.writeRuntimeProfile
runtime/pprof.writeGoroutine
runtime/pprof.(*Profile).WriteTo
net/http/pprof.handler.ServeHTTP
...
-----------+-------------------------------------------------------
详细分析:
| 函数名 | 说明 |
|---|---|
runtime.goroutineProfileWithLabels |
采集 goroutine profile 的函数 |
runtime/pprof.writeGoroutine |
写入 goroutine 数据 |
net/http/pprof.handler.ServeHTTP |
HTTP 处理函数 |
特点:
- ✓ 只有 1 个 goroutine
- ✓ 都在
runtime/pprof和net/http/pprof包中 - ✓ 是 pprof 工具自己创建的
结论 :✓ 正常,无需关注。
第 3-6 段:HTTP 服务 goroutine(正常)
-----------+-------------------------------------------------------
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
github.com/gin-gonic/gin.(*Engine).ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
-----------+-------------------------------------------------------
说明:
- 这些是不完整的堆栈片段
- 只显示了 HTTP 处理相关的部分
- 可能是堆栈被截断或不完整的显示
第 7 段:网络读取 goroutine(正常)
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
-----------+-------------------------------------------------------
分析:
| 函数名 | 说明 |
|---|---|
runtime.gopark |
goroutine 被挂起 |
runtime.netpollblock |
网络轮询阻塞 |
特点:
- ✓ 只有 1 个 goroutine
- ✓
netpollblock表示等待网络 I/O - ✓ 这是 HTTP 服务读取客户端请求的标准行为
结论 :✓ 正常,HTTP 服务在等待请求。
第 8-9 段:网络轮询 goroutine(正常)
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.waitIO
internal/poll.execIO
internal/poll.(*FD).Read
net.(*netFD).Read
net.(*conn).Read
net/http.(*connReader).Read
bufio.(*Reader).fill
bufio.(*Reader).Peek
net/http.(*conn).serve
-----------+-------------------------------------------------------
详细分析:
| 层级 | 函数名 | 说明 |
|---|---|---|
| 1 | runtime.gopark |
goroutine 被挂起 |
| 2 | runtime.netpollblock |
网络轮询阻塞 |
| 3-8 | internal/poll.* → net.* |
网络读取操作 |
| 9-12 | net/http.* → bufio.* |
HTTP 连接读取 |
特点:
- ✓ 只有 1 个 goroutine
- ✓ 完整的网络读取堆栈
- ✓ 这是 HTTP 连接读取器(
connReader)
结论 :✓ 正常,等待客户端请求数据。
第 10 段:HTTP 监听 goroutine(正常)
-----------+-------------------------------------------------------
1 runtime.gopark
runtime.netpollblock
internal/poll.runtime_pollWait
internal/poll.(*pollDesc).wait
internal/poll.waitIO
internal/poll.execIO
internal/poll.(*FD).acceptOne
internal/poll.(*FD).Accept
net.(*netFD).accept
net.(*TCPListener).accept
net.(*TCPListener).Accept
net/http.(*Server).Serve
net/http.(*Server).ListenAndServe
github.com/gin-gonic/gin.(*Engine).Run
main.main
runtime.main
-----------+-------------------------------------------------------
详细分析:
| 层级 | 函数名 | 说明 |
|---|---|---|
| 1-2 | runtime.gopark → runtime.netpollblock |
网络轮询阻塞 |
| 3-10 | internal/poll.* → net.(*TCPListener).Accept |
等待新连接 |
| 11-14 | net/http.(*Server).Serve → main.main |
HTTP 服务启动 |
特点:
- ✓ 只有 1 个 goroutine
- ✓ 完整的 HTTP 监听堆栈
- ✓ 最底层是
main.main,说明这是主 goroutine
结论 :✓ 正常,HTTP 服务主监听 goroutine。
分析依据
1 判断泄漏的依据
✗ 有泄漏:
- 数量异常:200 个相同堆栈的 goroutine
- 业务代码 :堆栈中包含
main.goroutineLeakTask.func1 - 阻塞类型 :
runtime.block表示被永久阻塞 - 堆栈深度:只有 3 层,说明卡在简单的阻塞点
✓ 正常 goroutine:
- 数量少:只有 1 个
- 系统代码 :都在
runtime.*、net/http.*、github.com/gin-gonic/gin.* - 阻塞类型 :
runtime.netpollblock表示网络等待 - 堆栈深度:10+ 层,说明有正常的调用链
2 阻塞类型判断
| 阻塞类型 | 含义 | 严重性 |
|---|---|---|
runtime.block |
被 select {} 阻塞 |
🔴 严重 - 泄漏 |
runtime.netpollblock |
网络轮询等待 | ✓ 正常 - I/O 操作 |
runtime.chanrecv |
等待 channel 接收 | ⚠️ 需检查 - 可能是正常等待 |
runtime.chansend |
等待 channel 发送 | ⚠️ 需检查 - 可能是死锁 |
traces 命令总结
1 traces 命令核心功能
| 功能 | 说明 |
|---|---|
| 显示所有 goroutine | 不像 top 只显示热点,traces 显示所有 |
| 按堆栈分组 | 相同堆栈的 goroutine 合并显示 |
| 完整调用链 | 从业务代码到底层运行时 |
| 识别阻塞原因 | 通过最底层函数判断阻塞类型 |
2 何时使用 traces
| 场景 | 推荐命令 |
|---|---|
| 快速查看热点 | top |
| 查看所有 goroutine | traces |
| 查看特定函数的 goroutine | traces main.* |
| 定位阻塞原因 | traces + 分析最底层函数 |
| 源码定位 | list main.* |
3 traces 命令对比
| 命令 | 输出内容 | 使用场景 |
|---|---|---|
top |
前 N 个热点函数 | 快速识别瓶颈 |
traces |
所有 goroutine 的完整堆栈 | 深入分析阻塞 |
list |
源码及采样位置 | 定位代码位置 |
web |
可视化调用图 | 整体理解 |
完整代码
三方包依赖 只有 gin
go
package main
import (
"fmt"
"net/http/pprof"
"runtime"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// 模拟一些性能问题的场景
// 场景1: CPU 密集型任务
func cpuIntensiveTask() {
result := 0
for i := 0; i < 1000000; i++ {
result += i * i
}
_ = result
}
// 场景2: 内存泄漏 - 持续增长
var memoryLeak []string
func memoryLeakTask() {
// 每次调用都添加数据,不释放
for i := 0; i < 10000; i++ {
memoryLeak = append(memoryLeak, fmt.Sprintf("leak-data-%d-%s", i, time.Now().String()))
}
}
// 场景3: Goroutine 泄漏 - 持续创建不关闭
var wg sync.WaitGroup
func goroutineLeakTask() {
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// 故意不使用 channel 或 done channel
// 这里会永远阻塞
select {}
}(i)
time.Sleep(10 * time.Millisecond)
}
}
// 场景4: 锁竞争
var (
sharedMap = make(map[int]int)
mapMutex sync.Mutex
mapWaitGroup sync.WaitGroup
)
func lockContentionTask() {
// 多个 goroutine 竞争同一个锁
for i := 0; i < 50; i++ {
mapWaitGroup.Add(1)
go func(n int) {
defer mapWaitGroup.Done()
for j := 0; j < 100; j++ {
mapMutex.Lock()
sharedMap[n] = j
time.Sleep(1 * time.Millisecond) // 增加锁持有时间
mapMutex.Unlock()
time.Sleep(1 * time.Millisecond)
}
}(i)
}
mapWaitGroup.Wait()
}
// 场景5: 堆内存分配
func heapAllocationTask() {
data := make([]byte, 10*1024*1024) // 分配 10MB
for i := range data {
data[i] = byte(i % 256)
}
time.Sleep(100 * time.Millisecond)
_ = data
}
func setupPPProf(r *gin.Engine) {
// pprof 路由组
pprofGroup := r.Group("/debug/pprof")
{
// pprof 指标页面
pprofGroup.GET("/", gin.WrapF(pprof.Index))
pprofGroup.GET("/cmdline", gin.WrapF(pprof.Cmdline))
pprofGroup.GET("/profile", gin.WrapF(pprof.Profile))
pprofGroup.POST("/profile", gin.WrapF(pprof.Profile))
pprofGroup.GET("/symbol", gin.WrapF(pprof.Symbol))
pprofGroup.POST("/symbol", gin.WrapF(pprof.Symbol))
pprofGroup.GET("/trace", gin.WrapF(pprof.Trace))
pprofGroup.POST("/trace", gin.WrapF(pprof.Trace))
// 性能指标 - 使用 HandlerWrapper
pprofGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
pprofGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
pprofGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
pprofGroup.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
pprofGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
// 分配器指标
pprofGroup.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
}
}
func main() {
// 创建 Gin 路由引擎
r := gin.Default()
// 设置 pprof 路由
setupPPProf(r)
// 演示接口
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, World!",
"status": "running",
})
})
// 场景演示接口
r.GET("/cpu-demo", func(c *gin.Context) {
// 设置 pprof 采样率,默认 100Hz
runtime.SetCPUProfileRate(100)
cpuIntensiveTask()
c.JSON(200, gin.H{"message": "CPU intensive task completed"})
})
r.GET("/memory-leak-demo", func(c *gin.Context) {
memoryLeakTask()
c.JSON(200, gin.H{
"message": "Memory leak task completed",
"current_size": len(memoryLeak),
})
})
r.GET("/goroutine-leak-demo", func(c *gin.Context) {
goroutineLeakTask()
c.JSON(200, gin.H{
"message": "Goroutine leak task completed",
})
})
r.GET("/lock-contention-demo", func(c *gin.Context) {
lockContentionTask()
c.JSON(200, gin.H{
"message": "Lock contention task completed",
})
})
r.GET("/heap-allocation-demo", func(c *gin.Context) {
heapAllocationTask()
c.JSON(200, gin.H{
"message": "Heap allocation task completed",
})
})
// 系统信息接口
r.GET("/stats", func(c *gin.Context) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
c.JSON(200, gin.H{
"goroutines": runtime.NumGoroutine(),
"alloc_memory": m.Alloc / 1024 / 1024, // MB
"total_alloc": m.TotalAlloc / 1024 / 1024, // MB
"sys_memory": m.Sys / 1024 / 1024, // MB
"gc_count": m.NumGC,
"heap_objects": m.HeapObjects,
"heap_inuse": m.HeapInuse / 1024 / 1024, // MB
"stack_inuse": m.StackInuse / 1024 / 1024, // MB
})
})
// 说明页面
r.GET("/", func(c *gin.Context) {
html := `
<!DOCTYPE html>
<html>
<head>
<title>Gin + pprof 演示</title>
<style>
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 20px auto; padding: 20px; }
h1 { color: #333; }
h2 { color: #666; margin-top: 30px; }
.section { margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 5px; }
.endpoint { background: white; padding: 10px; margin: 10px 0; border-left: 4px solid #4CAF50; }
code { background: #e0e0e0; padding: 2px 6px; border-radius: 3px; }
.warning { border-left-color: #ff9800; }
.danger { border-left-color: #f44336; }
.info { border-left-color: #2196F3; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f0f0f0; }
</style>
</head>
<body>
<h1>🚀 Gin + net/http/pprof 性能分析演示</h1>
<div class="section info">
<h2>📊 pprof 分析端点</h2>
<table>
<tr><th>端点</th><th>说明</th><th>用途</th></tr>
<tr><td>/debug/pprof/</td><td>pprof 首页</td><td>查看所有可用的分析端点</td></tr>
<tr><td>/debug/pprof/profile</td><td>CPU profile</td><td>CPU 性能分析,需要指定 seconds 参数</td></tr>
<tr><td>/debug/pprof/heap</td><td>堆内存 profile</td><td>内存分配和使用情况</td></tr>
<tr><td>/debug/pprof/goroutine</td><td>goroutine profile</td><td>当前所有 goroutine 的堆栈</td></tr>
<tr><td>/debug/pprof/block</td><td>block profile</td><td>阻塞操作分析</td></tr>
<tr><td>/debug/pprof/mutex</td><td>mutex profile</td><td>互斥锁竞争分析</td></tr>
<tr><td>/debug/pprof/allocs</td><td>分配 profile</td><td>内存分配统计</td></tr>
<tr><td>/debug/pprof/trace</td><td>trace</td><td>执行跟踪</td></tr>
</table>
</div>
<div class="section">
<h2>🎯 演示接口</h2>
<div class="endpoint">
<strong>GET /cpu-demo</strong><br>
<code>CPU 密集型任务演示</code><br>
用于测试 CPU 性能分析
</div>
<div class="endpoint danger">
<strong>GET /memory-leak-demo</strong><br>
<code>内存泄漏演示 ⚠️</code><br>
每次调用会增加约 10MB 内存,用于分析内存泄漏
</div>
<div class="endpoint danger">
<strong>GET /goroutine-leak-demo</strong><br>
<code>goroutine 泄漏演示 ⚠️</code><br>
每次调用会创建 100 个不退出的 goroutine
</div>
<div class="endpoint warning">
<strong>GET /lock-contention-demo</strong><br>
<code>锁竞争演示</code><br>
模拟多个 goroutine 竞争同一把锁
</div>
<div class="endpoint">
<strong>GET /heap-allocation-demo</strong><br>
<code>堆内存分配演示</code><br>
每次调用分配 10MB 堆内存
</div>
<div class="endpoint info">
<strong>GET /stats</strong><br>
<code>运行时统计</code><br>
查看当前内存和 goroutine 等统计信息
</div>
</div>
<div class="section">
<h2>📈 如何使用 pprof 分析</h2>
<h3>1. CPU 性能分析</h3>
<pre><code># 采集 30 秒的 CPU profile
curl -o cpu.prof "http://localhost:8080/debug/pprof/profile?seconds=30"
# 使用 go tool pprof 分析
go tool pprof cpu.prof
# 在 pprof 交互式命令中:
top # 查看 CPU 占用最高的函数
top10 # 查看前 10 个
list 函数名 # 查看具体函数的代码行
web # 生成调用图(需要 graphviz)
pdf # 生成 PDF 调用图</code></pre>
<h3>2. 内存分析</h3>
<pre><code># 采集内存 profile
curl -o heap.prof http://localhost:8080/debug/pprof/heap
# 分析内存使用
go tool pprof heap.prof
# 常用命令:
top # 查看内存占用
top -alloc_objects # 按分配对象数排序
list main.函数名 # 查看具体函数的内存分配
web # 查看调用图
# 查看 inuse 对象(当前还在使用的内存)
go tool pprof -sample_index=inuse_space heap.prof
go tool pprof -sample_index=inuse_objects heap.prof</code></pre>
<h3>3. Goroutine 分析</h3>
<pre><code># 采集 goroutine profile
curl -o goroutine.prof http://localhost:8080/debug/pprof/goroutine
# 分析 goroutine
go tool pprof goroutine.prof
# 查看所有 goroutine 的堆栈
go tool pprof -http=:8081 goroutine.prof
# 常用命令:
traces # 查看所有 goroutine 的堆栈跟踪
list main.函数名</code></pre>
<h3>4. 锁竞争分析</h3>
<pre><code># 采集锁竞争 profile
curl -o mutex.prof http://localhost:8080/debug/pprof/mutex
# 分析锁竞争
go tool pprof mutex.prof
top # 查看锁竞争最严重的地方</code></pre>
<h3>5. 可视化分析</h3>
<pre><code># 直接在浏览器中查看(推荐)
go tool pprof -http=:8081 cpu.prof
go tool pprof -http=:8081 heap.prof
# 在浏览器打开 http://localhost:8081 查看:
# - Top: 热点函数列表
# - Graph: 调用关系图
# - Flame Graph: 火焰图
# - Source: 源码视图</code></pre>
</div>
<div class="section warning">
<h2>⚠️ 性能问题识别指南</h2>
<h3>CPU 高占用</h3>
<ul>
<li>使用 <code>/cpu-demo</code> 接口触发</li>
<li>采集 30 秒 CPU profile</li>
<li>查看 top 函数,找出 CPU 占用最高的函数</li>
<li>使用 list 命令查看具体代码行</li>
</ul>
<h3>内存泄漏</h3>
<ul>
<li>多次调用 <code>/memory-leak-demo</code> 接口</li>
<li>采集 heap profile,指定 <code>?debug=1</code> 查看详细信息</li>
<li>查看 inuse_space 和 inuse_objects</li>
<li>找出不释放的内存分配点</li>
</ul>
<h3>Goroutine 泄漏</h3>
<ul>
<li>多次调用 <code>/goroutine-leak-demo</code> 接口</li>
<li>采集 goroutine profile</li>
<li>使用 traces 命令查看所有 goroutine 堆栈</li>
<li>找出不退出的 goroutine</li>
</ul>
<h3>锁竞争</h3>
<ul>
<li>调用 <code>/lock-contention-demo</code> 接口</li>
<li>采集 mutex profile</li>
<li>查看锁等待时间最长的函数</li>
<li>考虑优化锁粒度或使用无锁数据结构</li>
</ul>
</div>
<div class="section info">
<h2>🔧 编译时开启性能分析支持</h2>
<pre><code># 确保编译时启用了必要的 race 和 profile 支持
go build -race -o app main.go
# 或者使用 Makefile
make build</code></pre>
</div>
</body>
</html>
`
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, html)
})
fmt.Println(`
╔════════════════════════════════════════════════════════════╗
║ Gin + pprof 性能分析演示已启动 ║
╠════════════════════════════════════════════════════════════╣
║ 访问地址: http://localhost:8080 ║
║ pprof 端点: http://localhost:8080/debug/pprof/ ║
╠════════════════════════════════════════════════════════════╣
║ 演示接口: ║
║ - GET /cpu-demo CPU 密集型任务 ║
║ - GET /memory-leak-demo 内存泄漏演示 ⚠️ ║
║ - GET /goroutine-leak-demo goroutine 泄漏演示 ⚠️ ║
║ - GET /lock-contention-demo 锁竞争演示 ║
║ - GET /heap-allocation-demo 堆内存分配演示 ║
║ - GET /stats 运行时统计 ║
╠════════════════════════════════════════════════════════════╣
║ pprof 分析命令: ║
║ 1. 采集: curl -o cpu.prof "http://localhost:8080/debug/pprof/profile?seconds=30"║
║ 2. 分析: go tool pprof cpu.prof ║
║ 3. 可视化: go tool pprof -http=:8081 cpu.prof ║
╚════════════════════════════════════════════════════════════╝
`)
// 服务监听
err := r.Run("localhost:8080")
if err != nil {
panic(err)
}
}