go核武器——pprof 性能分析

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

说明:

  1. 分析 和 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),功能更强大,体验更好! 🚀

提要:

  1. 为什么有些函数 flat = 0?
    因为 这个函数 自己没有执行代码,只是调用了其他函数。
  2. 优化的重点:
sh 复制代码
  1. 你的业务代码 (main.*)          ← 优化重点
  2. 标准库 (net, os, time)        ← 了解即可
  3. runtime/*                    ← 了解即可
  4. syscall/*                    ← 忽略
  5. internal/*                   ← 忽略

二、内存分析

快照式: 记录当前时刻的内存分配状态

关键要点:内存泄漏 和 分配热点

四个 内存分析的 核心指标

sh 复制代码
inuse  vs  alloc
  ↓          ↓
当前使用的  所有分配的(包括已释放的)

space  vs  objects
  ↓          ↓
  大小      对象数量
  1. inuse_space (最常用, 90% 的情况下,只需inuse_space! 🎯)

    含义: 当前还在使用的 内存大小, 没有被 GC 回收的内存

    通俗解释:

    就像你钱包里的钱:

    • 你现在还有多少钱 = inuse_space
    • 不管你花掉了多少,只看你现在还剩多少
  2. inuse_objects

    含义:当前还在使用的 对象数量, 没有被 GC 回收的对象个数

    通俗解释:

    就像你房间里的物品:

    • 你现在还有多少件物品 = inuse_objects
    • 不管你扔掉了多少,只看你现在还剩多少
  3. alloc_space

    含义:程序运行以来分配的 总内存大小, 包括已经被 GC 回收的内存。

    alloc_space 的时间基准是: 自程序启动以来 的 累计值

通俗解释:

就像你总共赚了多少钱:

  • 不管你花了多少,只看你总共赚了多少
  • 即使钱花光了,你赚的钱也算在内
  1. 同理

内存大小 和 对象数量

场景举例

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

说明:

  1. 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 可以更准确地找出真正的内存泄漏

  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 相同堆栈的意义

相同堆栈意味着

  1. 相同的执行路径:这些 goroutine 经过了相同的函数调用
  2. 相同的阻塞位置:都停在同一个函数的同一行
  3. 相同的创建原因:通常是由同一个地方创建的
  4. 相同的业务含义:都在做同一件事

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.* 系统函数

修复方案

  1. Worker Pool: 固定 goroutine 数量
  2. Context: 添加超时控制
  3. 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/pprofnet/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.goparkruntime.netpollblock 网络轮询阻塞
3-10 internal/poll.*net.(*TCPListener).Accept 等待新连接
11-14 net/http.(*Server).Servemain.main HTTP 服务启动

特点

  • ✓ 只有 1 个 goroutine
  • ✓ 完整的 HTTP 监听堆栈
  • ✓ 最底层是 main.main,说明这是主 goroutine

结论 :✓ 正常,HTTP 服务主监听 goroutine。


分析依据

1 判断泄漏的依据

✗ 有泄漏

  1. 数量异常:200 个相同堆栈的 goroutine
  2. 业务代码 :堆栈中包含 main.goroutineLeakTask.func1
  3. 阻塞类型runtime.block 表示被永久阻塞
  4. 堆栈深度:只有 3 层,说明卡在简单的阻塞点

✓ 正常 goroutine

  1. 数量少:只有 1 个
  2. 系统代码 :都在 runtime.*net/http.*github.com/gin-gonic/gin.*
  3. 阻塞类型runtime.netpollblock 表示网络等待
  4. 堆栈深度: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)
	}
}
相关推荐
zhuhezhang3 小时前
一个用golang开发的文本对比工具
开发语言·后端·golang·wails
Reisentyan4 小时前
[backend]GoLang Learn Data Day 2
开发语言·后端·golang
Tony Bai17 小时前
Rust 看了流泪,AI 看了沉默:扒开 Go 泛型最让你抓狂的“残疾”类型推断
开发语言·人工智能·后端·golang·rust
ん贤18 小时前
AI 大模型落地系列|Eino 编排进阶篇:一文讲透编排(Chain 与 Graph)
人工智能·golang·编排·eino
GDAL20 小时前
BoltDB vs SQLite:极简高并发、低配置场景下的终极对比
golang·sqlite·boltdb
ruxingli1 天前
GoLang的并发如何避免死锁
开发语言·后端·golang
暴躁小师兄数据学院1 天前
【WEB3.0零基础转行笔记】go编程篇-第12讲:go-zero入门实战
开发语言·笔记·golang·web3·区块链
念何架构之路1 天前
Go语言表达式的求值顺序
开发语言·后端·golang
低调的JVM1 天前
Golang下kafka可观测数据采集组件Otelsarama详解
golang·kafka·可观测·opentelemetry