Delve (dlv) - Go 调试工具完整指南

Delve是Go语言专用调试工具,支持断点、单步执行和变量查看。通过go install安装后可用dlv debug调试程序,dlv test调试测试,支持命令行与VS Code等IDE集成,提升开发效率。

在Go语言开发中,调试是排查问题的重要环节。Delve(dlv)是专为Go设计的调试器,功能强大且使用方便。它支持断点、变量查看、单步执行等常见调试操作,特别适合在命令行或IDE中集成使用。

安装 Delve

 1# 方法1: 使用 go install (推荐)
 2go install github.com/go-delve/delve/cmd/dlv@latest
 3
 4# 方法2: 从源码安装
 5git clone https://github.com/go-delve/delve.git
 6cd delve
 7go install github.com/go-delve/delve/cmd/dlv
 8#
 9# 验证安装
10dlv version

实战案例:调试一个 Web API 服务

我们将创建一个有 bug 的 Web 服务,然后使用 dlv 来调试它。

项目结构

1debug-demo/
2├── main.go
3├── handlers/
4│   └── user.go
5├── models/
6│   └── user.go
7└── utils/
8    └── calculator.go

示例代码

main.go

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "net/http"
 7
 8    "debug-demo/handlers"
 9)
10
11func main() {
12    fmt.Println("Starting server on :8080...")
13
14    http.HandleFunc("/api/users", handlers.GetUsers)
15    http.HandleFunc("/api/user/", handlers.GetUserByID)
16    http.HandleFunc("/api/calculate", handlers.Calculate)
17
18    log.Fatal(http.ListenAndServe(":8080", nil))
19}

handlers/user.go

 1package handlers
 2
 3import (
 4    "encoding/json"
 5    "fmt"
 6    "net/http"
 7    "strconv"
 8    "strings"
 9
10    "debug-demo/models"
11    "debug-demo/utils"
12)
13
14var users = []models.User{
15    {ID: 1, Name: "Alice", Age: 25, Email: "alice@example.com"},
16    {ID: 2, Name: "Bob", Age: 30, Email: "bob@example.com"},
17    {ID: 3, Name: "Charlie", Age: 35, Email: "charlie@example.com"},
18}
19
20// GetUsers 获取所有用户 (有意留下 bug)
21func GetUsers(w http.ResponseWriter, r *http.Request) {
22    fmt.Println("GetUsers called")
23
24    // Bug: 这里会导致空指针
25    var result []models.User
26    for i := 0; i < len(users)+1; i++ { // 故意越界
27        if i < len(users) {
28            result = append(result, users[i])
29        }
30    }
31
32    w.Header().Set("Content-Type", "application/json")
33    json.NewEncoder(w).Encode(result)
34}
35
36// GetUserByID 根据ID获取用户 (有意留下 bug)
37func GetUserByID(w http.ResponseWriter, r *http.Request) {
38    fmt.Println("GetUserByID called:", r.URL.Path)
39
40    // 提取 ID
41    path := strings.TrimPrefix(r.URL.Path, "/api/user/")
42    id, err := strconv.Atoi(path)
43    if err != nil {
44        http.Error(w, "Invalid user ID", http.StatusBadRequest)
45        return
46    }
47
48    // Bug: 没有检查数组越界
49    user := users[id-1] // 如果 id=0 或 id>len(users) 会 panic
50
51    w.Header().Set("Content-Type", "application/json")
52    json.NewEncoder(w).Encode(user)
53}
54
55// Calculate 计算接口
56func Calculate(w http.ResponseWriter, r *http.Request) {
57    fmt.Println("Calculate called")
58
59    aStr := r.URL.Query().Get("a")
60    bStr := r.URL.Query().Get("b")
61
62    a, _ := strconv.Atoi(aStr)
63    b, _ := strconv.Atoi(bStr)
64
65    // Bug: 除法可能除以0
66    result := utils.Divide(a, b)
67
68    response := map[string]interface{}{
69        "a":      a,
70        "b":      b,
71        "result": result,
72    }
73
74    w.Header().Set("Content-Type", "application/json")
75    json.NewEncoder(w).Encode(response)
76}

models/user.go

 1package models
 2
 3type User struct {
 4    ID    int    `json:"id"`
 5    Name  string `json:"name"`
 6    Age   int    `json:"age"`
 7    Email string `json:"email"`
 8}
 9
10// ValidateAge 验证年龄 (有意留下 bug)
11func (u *User) ValidateAge() bool {
12    // Bug: 逻辑错误
13    return u.Age > 0 && u.Age < 18 // 应该是 < 150
14}

utils/calculator.go

 1package utils
 2
 3import "fmt"
 4
 5// Divide 除法 (有意留下 bug)
 6func Divide(a, b int) float64 {
 7    fmt.Printf("Dividing %d by %d\\\\n", a, b)
 8
 9    // Bug: 没有检查除以0
10    return float64(a) / float64(b)
11}
12
13// Add 加法
14func Add(a, b int) int {
15    result := a + b
16    fmt.Printf("Adding %d + %d = %d\\\\n", a, b, result)
17    return result
18}
19
20// Multiply 乘法 (有意留下 bug)
21func Multiply(a, b int) int {
22    result := 0
23    for i := 0; i < b; i++ {
24        result = Add(result, a)
25    }
26    return result
27}

Delve 调试实战

1. 基本启动和运行

 1# 编译并启动调试
 2dlv debug main.go
 3
 4# 或者先编译,再调试二进制文件
 5go build -o myapp main.go
 6dlv exec ./myapp
 7
 8# 调试测试
 9dlv test
10
11# 附加到正在运行的进程
12dlv attach <pid>

2. 常用 dlv 命令

进入 dlv 后的命令:

 1# === 断点相关 ===
 2break (b)                    # 设置断点
 3  b main.main                # 在 main 函数设置断点
 4  b handlers/user.go:25      # 在文件第25行设置断点
 5  b handlers.GetUserByID     # 在函数设置断点
 6
 7breakpoints (bp)             # 查看所有断点
 8clear <id>                   # 删除断点
 9clearall                     # 删除所有断点
10
11# === 执行控制 ===
12continue (c)                 # 继续执行到下一个断点
13next (n)                     # 单步执行(不进入函数)
14step (s)                     # 单步执行(进入函数)
15stepout (so)                 # 跳出当前函数
16restart (r)                  # 重启程序
17
18# === 变量查看 ===
19print (p) <var>              # 打印变量值
20  p users                    # 打印 users 变量
21  p users[0]                 # 打印数组元素
22  p user.Name                # 打印结构体字段
23
24locals                       # 显示所有局部变量
25args                         # 显示函数参数
26vars                         # 显示包级变量
27whatis <var>                 # 显示变量类型
28
29# === 调用栈 ===
30goroutines (grs)             # 显示所有 goroutine
31goroutine (gr) <id>          # 切换到指定 goroutine
32stack (bt)                   # 显示调用栈
33frame <n>                    # 切换到指定栈帧
34
35# === 其他 ===
36list (ls)                    # 显示源代码
37help                         # 显示帮助
38quit (q)                     # 退出

3. 调试场景演示

场景1: 调试数组越界问题

 1# 启动调试
 2$ dlv debug main.go
 3
 4(dlv) break handlers.GetUsers
 5Breakpoint 1 set at 0x... for debug-demo/handlers.GetUsers()
 6
 7(dlv) continue
 8Starting server on :8080...
 9
10# 在另一个终端发送请求
11$ curl <http://localhost:8080/api/users>
12
13# 回到 dlv
14(dlv) # 断点命中
15> debug-demo/handlers.GetUsers() ./handlers/user.go:18
16
17(dlv) list
18   13:    {ID: 3, Name: "Charlie", Age: 35, Email: "charlie@example.com"},
19   14: }
20   15:
21   16: func GetUsers(w http.ResponseWriter, r *http.Request) {
22   17:    fmt.Println("GetUsers called")
23=> 18:
24   19:    var result []models.User
25   20:    for i := 0; i < len(users)+1; i++ { // 故意越界
26   21:        if i < len(users) {
27   22:            result = append(result, users[i])
28
29(dlv) print users
30[]debug-demo/models.User len: 3, cap: 3, [
31    {ID: 1, Name: "Alice", Age: 25, Email: "alice@example.com"},
32    {ID: 2, Name: "Bob", Age: 30, Email: "bob@example.com"},
33    {ID: 3, Name: "Charlie", Age: 35, Email: "charlie@example.com"},
34]
35
36(dlv) break 20
37Breakpoint 2 set at 0x... for debug-demo/handlers.GetUsers()
38
39(dlv) continue
40
41(dlv) print i
420
43
44(dlv) next
45(dlv) next
46(dlv) print i
471
48
49(dlv) print result
50[]debug-demo/models.User len: 1, cap: 1, [
51    {ID: 1, Name: "Alice", Age: 25, Email: "alice@example.com"},
52]
53
54# 继续观察,发现 i=3 时会访问 users[3],越界!
55(dlv) condition 2 i == 3
56(dlv) continue
57
58(dlv) print i
593
60(dlv) print len(users)
613
62# 发现问题:i < len(users)+1 应该改为 i < len(users)

场景2: 调试除零错误

 1$ dlv debug main.go
 2
 3(dlv) break utils.Divide
 4Breakpoint 1 set at 0x...
 5
 6(dlv) continue
 7
 8# 发送请求:curl "<http://localhost:8080/api/calculate?a=10&b=0>"
 9
10(dlv) # 断点命中
11> debug-demo/utils.Divide() ./utils/calculator.go:6
12
13(dlv) args
14a = 10
15b = 0
16
17(dlv) print b
180
19
20(dlv) next
21(dlv) next
22
23# 可以看到即将执行除法
24(dlv) list
25    6: func Divide(a, b int) float64 {
26    7:     fmt.Printf("Dividing %d by %d\\\\n", a, b)
27    8:
28=>  9:     return float64(a) / float64(b)  // 即将除以0!
29   10: }
30
31(dlv) # 这里应该添加检查
32# if b == 0 { return 0 或 报错 }

场景3: 调试 Goroutine

 1// 添加到 main.go
 2func backgroundTask() {
 3    ticker := time.NewTicker(2 * time.Second)
 4    defer ticker.Stop()
 5
 6    for {
 7        select {
 8        case <-ticker.C:
 9            fmt.Println("Background task running...")
10            // 可能有 bug 的代码
11        }
12    }
13}
14
15func main() {
16    go backgroundTask() // 启动后台任务
17    // ... rest of code
18}

调试 goroutine:

 1(dlv) goroutines
 2[6 goroutines]
 3* Goroutine 1 - User: ./main.go:15 main.main (0x...)
 4  Goroutine 2 - User: /usr/local/go/src/runtime/proc.go:367 runtime.gopark (0x...)
 5  Goroutine 3 - User: ./main.go:8 main.backgroundTask (0x...)
 6  Goroutine 4 - User: /usr/local/go/src/net/http/server.go:3070 ...
 7  ...
 8
 9(dlv) goroutine 3
10Switched from 1 to 3
11
12(dlv) stack
130  0x... in main.backgroundTask
14   at ./main.go:8
151  0x... in runtime.goexit
16   at /usr/local/go/src/runtime/asm_amd64.s:1598
17
18(dlv) locals
19ticker = (*time.Ticker)(0x...)

4. 条件断点和监视点

 1# 条件断点:只在 id == 2 时中断
 2(dlv) break handlers.GetUserByID
 3(dlv) condition 1 id == 2
 4
 5# 或者在设置时直接指定
 6(dlv) break handlers.GetUserByID if id == 2
 7
 8# 查看断点条件
 9(dlv) breakpoints
10Breakpoint 1 at 0x... for handlers.GetUserByID() with condition id == 2

5. 修改变量值

 1(dlv) break handlers.GetUserByID
 2(dlv) continue
 3
 4(dlv) print id
 52
 6
 7# 修改变量值
 8(dlv) set id = 1
 9(dlv) print id
101
11
12# 继续执行,现在使用新的 id 值
13(dlv) continue

6. 调用函数

 1(dlv) break handlers.GetUsers
 2(dlv) continue
 3
 4# 在断点处调用函数
 5(dlv) call len(users)
 63
 7
 8(dlv) call utils.Add(10, 20)
 930
10
11# 调用方法
12(dlv) call users[0].ValidateAge()
13false

高级技巧

1. 使用配置文件

创建 .dlv/config.yml:

1# 自动设置的断点
2break:
3  - handlers.GetUserByID
4  - utils.Divide
5
6# 替换路径(适用于容器调试)
7substitute-path:
8  - {from: "/go/src/app", to: "/Users/you/project"}

2. 远程调试

1# 服务器端启动 headless 模式
2dlv debug --headless --listen=:2345 --api-version=2 main.go
3
4# 本地连接
5dlv connect localhost:2345

3. VS Code 集成

.vscode/launch.json:

 1{
 2    "version": "0.2.0",
 3    "configurations": [
 4        {
 5            "name": "Launch Package",
 6            "type": "go",
 7            "request": "launch",
 8            "mode": "debug",
 9            "program": "${workspaceFolder}",
10            "env": {},
11            "args": []
12        },
13        {
14            "name": "Attach to Process",
15            "type": "go",
16            "request": "attach",
17            "mode": "local",
18            "processId": 0
19        },
20        {
21            "name": "Connect to Remote",
22            "type": "go",
23            "request": "attach",
24            "mode": "remote",
25            "remotePath": "${workspaceFolder}",
26            "port": 2345,
27            "host": "localhost"
28        }
29    ]
30}

4. GoLand/IDEA 集成

GoLand 内置了对 Delve 的支持:

  1. 点击行号左侧设置断点
  2. 点击 Debug 按钮(小虫子图标)
  3. 使用 Variables 窗口查看变量
  4. 使用 Watches 添加监视表达式

调试最佳实践

1. 编译时保留调试信息

1# 不要使用 -ldflags="-s -w",这会移除调试信息
2go build -gcflags="all=-N -l" -o myapp main.go
3
4# 推荐的调试编译选项
5go build -gcflags="all=-N -l" main.go

2. 使用日志辅助调试

 1import "log"
 2
 3func GetUserByID(w http.ResponseWriter, r *http.Request) {
 4    log.Printf("[DEBUG] GetUserByID called: %s", r.URL.Path)
 5
 6    path := strings.TrimPrefix(r.URL.Path, "/api/user/")
 7    log.Printf("[DEBUG] Extracted ID string: %s", path)
 8
 9    id, err := strconv.Atoi(path)
10    if err != nil {
11        log.Printf("[ERROR] Invalid ID: %v", err)
12        http.Error(w, "Invalid user ID", http.StatusBadRequest)
13        return
14    }
15    log.Printf("[DEBUG] Parsed ID: %d", id)
16
17    // ... rest of code
18}

3. 常见调试场景速查

 1# Panic 调试
 2(dlv) break runtime.gopanic
 3(dlv) continue
 4(dlv) stack
 5
 6# 死锁调试
 7(dlv) goroutines
 8(dlv) goroutine <id>
 9(dlv) stack
10
11# 内存泄漏调试
12(dlv) break main.main
13(dlv) continue
14# 使用 pprof 结合分析
15
16# 性能问题调试
17(dlv) break mySlowFunction
18(dlv) continue
19(dlv) # 检查变量和逻辑

完整调试会话示例

 1$ dlv debug main.go
 2
 3Type 'help' for list of commands.
 4
 5(dlv) break main.main
 6Breakpoint 1 set at 0x10a1234 for main.main() ./main.go:10
 7
 8(dlv) break handlers.GetUserByID
 9Breakpoint 2 set at 0x10a5678 for handlers.GetUserByID() ./handlers/user.go:28
10
11(dlv) continue
12> main.main() ./main.go:10 (hits goroutine(1):1 total:1)
13     5: import (
14     6:     "fmt"
15     7:     "log"
16     8:     "net/http"
17     9:
18=>  10:     "debug-demo/handlers"
19    11: )
20    12:
21    13: func main() {
22    14:     fmt.Println("Starting server on :8080...")
23    15:
24
25(dlv) next
26(dlv) next
27(dlv) next
28Starting server on :8080...
29
30(dlv) continue
31
32# 在另一个终端: curl <http://localhost:8080/api/user/2>
33
34> handlers.GetUserByID() ./handlers/user.go:28
35    23:     Email string `json:"email"`
36    24: }
37    25:
38    26: func GetUserByID(w http.ResponseWriter, r *http.Request) {
39    27:     fmt.Println("GetUserByID called:", r.URL.Path)
40=>  28:
41    29:     path := strings.TrimPrefix(r.URL.Path, "/api/user/")
42    30:     id, err := strconv.Atoi(path)
43    31:     if err != nil {
44    32:         http.Error(w, "Invalid user ID", http.StatusBadRequest)
45    33:         return
46
47(dlv) print r.URL.Path
48"/api/user/2"
49
50(dlv) next
51(dlv) next
52(dlv) print path
53"2"
54
55(dlv) next
56(dlv) print id
572
58(dlv) print err
59error nil
60
61(dlv) next
62(dlv) print len(users)
633
64
65(dlv) # 如果 id=4 会越界,我们可以测试
66(dlv) set id = 4
67(dlv) next
68panic: runtime error: index out of range [3] with length 3
69
70(dlv) stack
710  0x... in runtime.gopanic
72   at /usr/local/go/src/runtime/panic.go:884
731  0x... in handlers.GetUserByID
74   at ./handlers/user.go:36
752  0x... in net/http.HandlerFunc.ServeHTTP
76   at /usr/local/go/src/net/http/server.go:2122
77
78(dlv) quit

总结

Delve 的核心命令:

  • break - 设置断点
  • continue - 继续执行
  • next - 单步执行
  • step - 进入函数
  • print - 查看变量
  • stack - 查看调用栈
  • goroutines - 查看 goroutine

调试流程:

  1. 设置断点
  2. 运行程序
  3. 检查变量和状态
  4. 单步执行找出问题
  5. 修复代码

掌握这些技巧,你就能高效地调试 Go 程序了!