Golang中Delve调试器输出不稳定是工具问题还是操作问题?

Golang中Delve调试器输出不稳定是工具问题还是操作问题? 我感觉自己像在吃疯狂药丸,似乎遗漏了什么极其明显的东西。之前我曾报告过一个Delve bug,结果却被重新归类为Go bug,这次我想确认是否有其他人遇到过这种情况,或者知道这是个已知问题(当然也可能是用户错误,这可能性最大)。

下面有更详细的版本,但简而言之

我在调试器中看到变量被报告为nil、默认值结构体和空字符串。变量在某一行看起来正常,到下一行就变成空值或nil。有人遇到过这种情况吗?

这是Go 1.11 rc1、GoLand 2018.2.1以及2018.2.1版本自带的Delve。

我搜索过Delve的问题记录,发现一些关于局部变量丢失的问题,但似乎特定于VS Code,不确定是否与此相关。也许我漏掉了已有的相关问题?

具体情况是:我在客户端启动测试,这会启动一个新服务器。然后我附加到服务器进程并逐步执行服务器端断点。我在运行之间执行了go buildgo install,所以运行的服务器二进制文件应该™与我逐步调试的源代码同步。

编辑: 需要说明的是,我也尝试过使用-gcflags '-N -l'禁用编译器优化和内联进行构建——也许我遗漏了其他必要的标志?我还检查了系统上没有多个版本的服务器(这样客户端就不应该使用过时的旧服务器二进制文件)


以下是包含代码和截图的详细版本,希望能帮助其他人发现操作上的问题:

我从*http.Request中检索自定义请求结构体。这部分工作正常。结构体定义如下:

type JarReq struct {
	request
	Jar          domain.Jar `json:"jar"`
	AuthProvider string     `json:"authProvider"`
}

在函数A中,我运行jarReq, err := getJarReq(r),其实现如下:

func getJarReq(r *http.Request) (restapi.JarReq, error) {
	var jar restapi.JarReq
	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
	if err != nil {
		return jar, err
	}

	defer r.Body.Close()

	if err := json.Unmarshal(body, &jar); err != nil {
		return jar, err
	}
	return jar, nil
}

然后在函数A中调用这个错误处理函数:

func HandleReqErr(err error, w http.ResponseWriter) {
	if err == nil {
		return
	}
	util.HandleErr(err, "CreateUserErr")
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(422) // unprocessable entity
	if err := json.NewEncoder(w).Encode(err); err != nil {
		util.HandleErr(err, "CreateUserErr")
	}
}

由于err为nil,它直接返回。

接着我将jarReq传递给另一个函数:jarRes, err := createJar(jarReq)

其定义如下:

func createJar(jarReq restapi.JarReq) (jarRes restapi.ExportedJarRes, err error) {
	jar := jarReq.Jar
	repo, err := domain.NewJarRepo(env.App.Database)
	eRes := restapi.ExportedJarRes{}
	res, err := handlers.CreateModel(repo, &jar, &eRes)
	return *res.(*restapi.ExportedJarRes), err
}

但一旦我步入createJar,调试器中的jarReq就显示为nil

为了调试这个问题,我在createJar()中打印了jarReq,如下所示:

func createJar(jarReq restapi.JarReq) (jarRes restapi.ExportedJarRes, err error) {
	msg := fmt.Sprintf("%v", jarReq)
	fmt.Println(msg)
	jar := jarReq.Jar
	repo, err := domain.NewJarRepo(env.App.Database)
	eRes := restapi.ExportedJarRes{}
	res, err := handlers.CreateModel(repo, &jar, &eRes)
	return *res.(*restapi.ExportedJarRes), err
}

我可以看到当我设置msg时,值看起来是正确的。但在调试器中jarReq仍然是nil。然后当我前进一步时,调试器中出现一个新变量:r·4——也是nil,类型为JarRepo(正如你在下一行看到的,我正要获取这个值,但这出现在jar := jarReq.Jar行的断点处,在我执行到domain.NewJarRepo()之前)

接着,msg变成了空字符串——在我进行任何其他调用或有机会以任何方式修改该值之前(据我所见!)以下是几张截图。注意看起来正常的msg,好像Sprintf确实从jarReq获取了内容,但jarReq却是nil。还要注意在我给msg赋值之前,jarReq就显示为nil

nil1

现在注意前进一步——这是在fmt.Println(msg)行中断。msg现在是空字符串,jarReq仍然是nil

nil2

还要注意,如果我逐步执行到并经过jar := jarReq.Jar,我没有遇到nil指针解引用panic,而且jar看起来正常,所以它显然获取了值。像这样:

5

但如果我再前进一步,jar就变成了默认的空结构体。

nil3

由于调试器中的变量不断变成默认结构体、空字符串和nil,我沿着调用链打印了一些值,如上面的示例——这些值看起来确实正确(至少我检查过的那些),但当我逐步执行到下一行时,字符串就变成了空字符串。有人遇到过这种情况吗?我是不是遗漏了什么愚蠢的明显问题?

谢谢(抱歉篇幅过长)


更多关于Golang中Delve调试器输出不稳定是工具问题还是操作问题?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

哦是的,抱歉,我误解了你的回答。

我不知道如何提供帮助,也许值得针对 Delve 再开一个 bug。

PS:我假设上面 createJar 中忽略的错误只是为了简洁起见,对吗?

更多关于Golang中Delve调试器输出不稳定是工具问题还是操作问题?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好Liza,

我不是delve用户,所以可能帮不上太多忙。其中一个原因可能是并发问题,或者是因为Delve在网络操作期间无法正确停止程序。

能否请你链接一下你向Delve提交的错误报告?

另外,你的程序是否存在竞态条件?

我明白了。正如你正确调试的那样,这是因为你使用了插件包(因此涉及动态SO加载)。

这对Go来说相对较新,可能会导致你遇到的这类问题。

不确定目前是否有解决方案:也许可以使用github.com/natefitch/pie和多进程来重新实现你的插件…

我认为我们之间存在误解。我最初提交的问题与此问题无关——我们可以假装另一个问题在这个特定讨论中根本不存在。我并没有使用插件。我之所以提到最初的问题,只是因为它是我错误地归因于Delve的一个例子,所以在将另一个问题错误地归因于Delve之前,我想在这里确认是否有人遇到过这种情况或有其他想法。这两个问题彼此完全独立。

你好,

我还没有向Delve报告这个bug,之前我提到要在Delve上提交另一个bug,结果最终变成了这个Go问题。在向Delve提交另一个bug之前,我想确认这不是某些明显的用户错误。我仍然怀疑是编译器优化的问题,尽管我已经通过前面提到的标志在理论上禁用了它们。

是的,据我所知这个程序是没有数据竞争的(我所有测试都开启了竞态检测器,而且这里没有使用任何复杂的goroutine并发操作)。

这个问题通过以下步骤得以解决:

  • 重启我的机器
  • 安装 Go 1.11(之前使用的是 rc1 版本),最重要的是:
  • 使用正确的命令来禁用优化:-gcflags='all=-N -l'

出于某些原因,仅执行步骤 c) 是不够的。我不确定其他问题的根本原因是什么,但现在调试器的行为已经恢复正常。

对于将来,这个命令有助于确认我的二进制文件是否已编译优化:

objdump --dwarf executable | grep DW_AT_producer

在完成上述步骤后,我可以在每行的末尾看到 -N -l 输出(二进制文件的大小也如预期那样变大了)。

感谢您抽时间查看这个问题 😊 我最终确实在Delve上提交了一个错误报告,正如所料,这确实是编译器优化的问题——我使用了错误的标志,只禁用了主包的优化。显然,我应该使用的是 -gcflags='all=-N -l',而不是 -gcflags '-N -l'

不幸的是,即使在修复了上述问题后,我仍然遇到相同的情况。我仍然怀疑优化没有完全禁用。使用上述标志前后的二进制文件大小完全相同,我原本预期未优化的二进制文件会更大……也许我遗漏了其他问题。无论如何,这是Delve上的问题链接,后续讨论可能会在那里进行(其中还提到了GoLand对Delve优化函数警告的处理稍好一些,而不是仅仅向用户显示误导性的nil/零值变量):https://github.com/derekparker/delve/issues/1320

忽略错误并非为了简洁,这是一个原型,还有很多工作要做,因此存在大量错误处理和其他问题需要完成和解决。

这是一个典型的调试器优化问题,通常与Go编译器的优化机制有关。即使你使用了 -gcflags '-N -l' 禁用了部分优化,Delve在某些情况下仍然可能无法正确显示变量值。

问题分析:

  1. 变量在运行时实际有值(从你的打印输出可以确认)
  2. 调试器显示为nil或默认值
  3. 代码执行正常,没有panic

这是Delve在特定Go版本下的已知问题,特别是在处理函数参数和局部变量时。编译器可能会重用寄存器或栈位置,导致调试器无法正确追踪变量。

解决方案:

  1. 使用更完整的调试标志
// 构建时使用更完整的调试标志
go build -gcflags="all=-N -l" your_package
  1. 强制变量保持在作用域内
func createJar(jarReq restapi.JarReq) (jarRes restapi.ExportedJarRes, err error) {
    // 强制将参数复制到局部变量
    debugJarReq := jarReq
    msg := fmt.Sprintf("%v", debugJarReq)
    fmt.Println(msg)
    
    jar := debugJarReq.Jar
    repo, err := domain.NewJarRepo(env.App.Database)
    eRes := restapi.ExportedJarRes{}
    res, err := handlers.CreateModel(repo, &jar, &eRes)
    return *res.(*restapi.ExportedJarRes), err
}
  1. 检查Delve版本兼容性
# 确保使用与Go 1.11兼容的Delve版本
dlv version
  1. 尝试直接使用Delve命令行
# 直接附加到进程进行调试
dlv attach <pid>
  1. 使用指针传递避免值复制问题
func createJar(jarReq *restapi.JarReq) (jarRes restapi.ExportedJarRes, err error) {
    if jarReq == nil {
        return restapi.ExportedJarRes{}, errors.New("nil jarReq")
    }
    msg := fmt.Sprintf("%v", *jarReq)
    fmt.Println(msg)
    
    jar := jarReq.Jar
    repo, err := domain.NewJarRepo(env.App.Database)
    eRes := restapi.ExportedJarRes{}
    res, err := handlers.CreateModel(repo, &jar, &eRes)
    return *res.(*restapi.ExportedJarRes), err
}

这个问题在Go 1.11 + Delve的组合中确实存在,特别是在通过函数边界传递结构体值时。使用上述方法通常可以缓解调试器显示问题,同时保持代码的正常执行。

回到顶部