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 的支持:
- 点击行号左侧设置断点
- 点击 Debug 按钮(小虫子图标)
- 使用 Variables 窗口查看变量
- 使用 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
调试流程:
- 设置断点
- 运行程序
- 检查变量和状态
- 单步执行找出问题
- 修复代码
掌握这些技巧,你就能高效地调试 Go 程序了!