调试技巧

41.1 Delve 调试器

Delve 是 Go 语言专用的调试器,提供了强大的调试功能。

安装 Delve

# 安装 Delve
go install github.com/go-delve/delve/cmd/dlv@latest

# 验证安装
dlv version

启动调试

# 调试主程序
dlv debug

# 调试特定文件
dlv debug main.go

# 调试测试
dlv test

# 附加到运行中的进程
dlv attach <pid>

41.2 基本调试命令

常用命令

命令缩写说明
breakb设置断点
continuec继续执行
nextn单步执行(不进入函数)
steps单步执行(进入函数)
stepoutso跳出当前函数
printp打印变量
locals显示局部变量
args显示函数参数
goroutinesgrs显示所有 goroutine
goroutinegr切换 goroutine
stackbt显示调用栈
listl显示源代码
quitq退出调试

设置断点

# 在 main 函数设置断点
(dlv) break main.main

# 在特定文件行号设置断点
(dlv) break main.go:20

# 在特定函数设置断点
(dlv) break main.calculate

# 查看所有断点
(dlv) breakpoints

# 清除断点
(dlv) clear 1
(dlv) clearall

条件断点

# 设置条件断点
(dlv) break main.go:20
(dlv) condition 1 i > 10

# 当变量 i 大于 10 时触发断点

41.3 调试示例

示例程序

package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5}

    sum := 0
    for i, num := range numbers {
        result := calculate(i, num)
        sum += result
        fmt.Printf("i=%d, num=%d, result=%d, sum=%d\n", i, num, result, sum)
    }

    fmt.Println("Final sum:", sum)
}

func calculate(i, num int) int {
    return i * num
}

调试过程

$ dlv debug main.go

# 设置断点
(dlv) break main.main
(dlv) break main.go:12
(dlv) break calculate

# 运行到第一个断点 c

# 单步执行

# 查看变量
(dlv) print numbers
(dlv) print i
(dlv) print sum

# 进入函数

# 查看调用栈

# 查看局部变量

# 继续执行 c

41.4 变量查看

打印变量

# 打印简单变量
(dlv) print i
(dlv) print sum

# 打印切片
(dlv) print numbers
(dlv) print numbers[0]
(dlv) print len(numbers)

# 打印结构体
(dlv) print user
(dlv) print user.Name

# 打印指针
(dlv) print *ptr

查看局部变量和参数

# 显示当前函数的局部变量

# 显示当前函数的参数

# 显示所有变量
(dlv) vars

###修改变量

# 修改变量值
(dlv) set i = 10
(dlv) set sum = 100

41.5 Goroutine 调试

查看 Goroutine

# 显示所有 goroutine
(dlv) goroutines

# 切换到特定 goroutine
(dlv) goroutine 5

# 查看 goroutine 的调用栈
(dlv) goroutine 5 bt

# 查看 goroutine 的局部变量
(dlv) goroutine 5 locals

Goroutine 调试示例

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        fmt.Printf("Worker %d: %d\n", id, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("Done")
}

调试:

$ dlv debug

# 在 worker 函数设置断点
(dlv) break worker

# 运行 c

# 查看所有 goroutine
(dlv) goroutines

# 切换 goroutine 并查看状态
(dlv) goroutine 2
(dlv) bt
(dlv) locals

41.6 调用栈分析

查看调用栈

# 显示完整调用栈

# 显示指定深度的调用栈
(dlv) stack 10

# 切换栈帧
(dlv) frame 2

# 在特定栈帧查看变量
(dlv) frame 2 locals
(dlv) frame 2 args

调用栈示例

package main

func main() {
    result := level1()
    println(result)
}

func level1() int {
    return level2()
}

func level2() int {
    return level3()
}

func level3() int {
    return 42
}

调试:

$ dlv debug

# 在 level3 设置断点
(dlv) break level3

# 运行 c

# 查看调用栈

# 切换到 level2 的栈帧
(dlv) frame 1

# 切换到 level1 的栈帧
(dlv) frame 2

41.7 日志调试

使用 log 包

package main

import (
    "log"
    "os"
)

func main() {
    // 设置日志输出
    log.SetOutput(os.Stdout)
    log.SetFlags(log.LstdFlags | log.Lshortfile)

    log.Println("程序开始")

    result := calculate(10, 20)
    log.Printf("计算结果: %d\n", result)

    log.Println("程序结束")
}

func calculate(a, b int) int {
    log.Printf("calculate(%d, %d)\n", a, b)
    return a + b
}

条件日志

package main

import (
    "log"
    "os"
)

var debug = os.Getenv("DEBUG") == "true"

func debugLog(format string, args ...interface{}) {
    if debug {
        log.Printf("[DEBUG] "+format, args...)
    }
}

func main() {
    debugLog("程序开始")

    result := calculate(10, 20)
    debugLog("计算结果: %d", result)
}

func calculate(a, b int) int {
    debugLog("calculate(%d, %d)", a, b)
    return a + b
}

41.8 使用 fmt.Printf 调试

基本用法

package main

import "fmt"

func main() {
    data := map[string]int{"a": 1, "b": 2}

    // 打印值
    fmt.Printf("data: %v\n", data)

    // 打印类型和值
    fmt.Printf("data: %#v\n", data)

    // 打印类型
    fmt.Printf("type: %T\n", data)
}

格式化动词

动词说明
%v默认格式
%+v包含字段名
%#vGo 语法格式
%T类型
%d整数
%s字符串
%x十六进制
%p指针

41.9 panic 调试

使用 panic 定位问题

package main

func main() {
    data := []int{1, 2, 3}

    // 故意制造 panic 来定位问题
    if len(data) > 5 {
        panic("unexpected data length")
    }

    // 访问越界会自动 panic
    _ = data[10]
}

recover 捕获 panic

package main

import (
    "fmt"
    "runtime"
)

func safeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            // 获取调用栈
            buf := make([]byte, 4096)
            n := runtime.Stack(buf, false)
            err = fmt.Errorf("panic: %v\n%s", r, buf[:n])
        }
    }()

    // 可能 panic 的操作
    riskyOperation()
    return nil
}

func riskyOperation() {
    panic("something went wrong")
}

func main() {
    if err := safeOperation(); err != nil {
        fmt.Println("捕获到错误:")
        fmt.Println(err)
    }
}

41.10 IDE 调试

VS Code 配置

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}",
            "env": {
                "DEBUG": "true"
            },
            "args": []
        },
        {
            "name": "Attach to Process",
            "type": "go",
            "request": "attach",
            "mode": "local",
            "processId": 0
        }
    ]
}

调试快捷键

操作VS CodeGoLand
开始调试F5Shift+F9
单步跳过F10F8
单步进入F11F7
单步跳出Shift+F11Shift+F8
继续F5F9
停止Shift+F5Ctrl+F2

41.11 小结

本章详细介绍了 Go 语言的调试技巧:

  1. Delve 调试器:安装和基本使用
  2. 调试命令:断点、单步执行、变量查看
  3. 条件断点:根据条件触发断点
  4. Goroutine 调试:查看和切换 Goroutine
  5. 调用栈分析:查看和切换栈帧
  6. 日志调试:使用 log 包和条件日志
  7. panic 调试:使用 panic 和 recover 定位问题
  8. IDE 调试:VS Code 和 GoLand 配置

调试是开发过程中必不可少的技能,掌握调试技巧能让你更快地定位和解决问题。在下一章中,我们将开始实战项目的学习。