Go 博客

在 Go 1.12 中调试已部署的代码

David Chase
2019 年 3 月 21 日

引言

Go 1.11 和 Go 1.12 在帮助开发者调试部署到生产环境的相同优化二进制文件方面取得了重大进展。

随着 Go 编译器在生成更快的二进制文件方面变得越来越激进,我们在可调试性方面有所退步。在 Go 1.10 中,用户需要完全禁用优化才能从 Delve 等交互式工具获得良好的调试体验。但用户不应该为了可调试性而牺牲性能,尤其是在运行生产服务时。如果你的问题发生在生产环境中,你需要在生产环境中调试它,这不应该要求部署未优化的二进制文件。

对于 Go 1.11 和 1.12,我们专注于改进优化二进制文件(Go 编译器的默认设置)的调试体验。改进包括:

  • 更准确的值检查,特别是函数入口处的参数;
  • 更精确地识别语句边界,从而使单步调试时跳跃感更小,并且断点更经常地停在程序员期望的位置;
  • 以及 Delve 调用 Go 函数的初步支持(goroutine 和垃圾回收使得这比 C 和 C++ 中更棘手)。

使用 Delve 调试优化代码

Delve 是一个用于 x86 架构 Go 程序的调试器,支持 Linux 和 macOS。Delve 了解 goroutine 和其他 Go 特性,并提供了最佳的 Go 调试体验之一。Delve 也是 GoLandVS CodeVim 背后的调试引擎。

Delve 通常会使用 -gcflags "all=-N -l" 重新构建正在调试的代码,这会禁用内联和大多数优化。要使用 delve 调试优化代码,首先构建优化后的二进制文件,然后使用 dlv exec your_program 进行调试。或者,如果你有一个崩溃生成的 core 文件,可以使用 dlv core your_program your_core 来检查它。使用 Go 1.12 和最新的 Delve 版本,你应该能够在优化二进制文件中检查许多变量。

改进的值检查

当调试 Go 1.10 生成的优化二进制文件时,变量值通常完全不可用。相比之下,从 Go 1.11 开始,即使在优化二进制文件中,变量通常也可以检查,除非它们完全被优化掉。在 Go 1.11 中,编译器开始发出 DWARF 位置列表,以便调试器可以跟踪变量在寄存器中移动以及如何在不同的寄存器和栈槽中重构复杂对象。

改进的单步调试

这展示了在 Go 1.10 的调试器中单步调试一个简单函数的示例,其中缺陷(跳过和重复的行)用红色箭头突出显示。

这样的缺陷使得在程序中单步调试时很容易失去当前位置,并干扰设置断点。

Go 1.11 和 1.12 记录了语句边界信息,并且在优化和内联过程中更好地跟踪源代码行号。因此,在 Go 1.12 中,单步调试这段代码时会停在每一行,并且按照你期望的顺序进行。

函数调用

Delve 中的函数调用支持仍在开发中,但简单情况是可行的。例如

(dlv) call fib(6)
> main.main() ./hello.go:15 (PC: 0x49d648)
Values returned:
    ~r1: 8

前进之路

Go 1.12 是朝着改进优化二进制文件调试体验迈出的一步,我们计划进一步改进它。

可调试性和性能之间存在根本性的权衡,因此我们正在专注于最高优先级的调试缺陷,并努力收集自动化指标来监控我们的进展并发现退化。

我们正在专注于为调试器生成关于变量位置的正确信息,这样如果一个变量可以打印,它就能正确打印出来。我们还在研究如何让变量值在更多时候可用,特别是在调用点等关键位置,尽管在许多情况下改进这一点需要减慢程序执行速度。最后,我们正在努力改进单步调试:我们专注于处理 panic 时的单步顺序、循环周围的单步顺序,以及尽可能地遵循源代码顺序。

关于 macOS 支持的说明

Go 1.11 开始压缩调试信息以减小二进制文件大小。Delve 原生支持此功能,但 LLDB 和 GDB 在 macOS 上都不支持压缩的调试信息。如果您使用 LLDB 或 GDB,有两种解决方法:使用 -ldflags=-compressdwarf=false 构建二进制文件,或者使用 splitdwarf (go get golang.org/x/tools/cmd/splitdwarf) 解压缩现有二进制文件中的调试信息。

下一篇文章:Go 2018 年度调查结果
上一篇文章:使用 Go Modules
博客索引