Go version
go version go1.25.0 darwin/arm64
Output of go env in your module/workspace:
AR='ar'
CC='cc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='c++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHEPROG=''
GODEBUG=''
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/7l/x6164vd11g73syllmzrm8nkc0000gp/T/go-build3087497557=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOOS='darwin'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.25.0/libexec'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.25.0/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
package main
import (
"fmt"
"io"
"net/http"
)
type (
A struct {
}
ArgsA struct {
URL string
}
)
func (a *A) MethodA(args *ArgsA) (string, error) {
resp, err := http.Get(args.URL)
if err != nil {
return "", nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil
}
return string(body), nil
}
type (
B struct {
a *A
}
ArgsB struct {
URL string
}
)
func (b *B) MethodB(args *ArgsB) error {
r, err := b.a.MethodA(&ArgsA{
URL: args.URL,
})
if err != nil {
return err
}
fmt.Println(r)
return nil
}
func main() {
b := &B{
a: &A{},
}
if err := b.MethodB(&ArgsB{
URL: "https://jsonplaceholder.typicode.com/posts",
}); err != nil {
panic(err)
}
}
There is a reproducer and a binary.
What did you see happen?
I launched debug session via default Run and Debug button at VS Code without launch.json
before MethodA invocation, got expected value of args.URL:
after MethodA invocation, got unpredictable value. It can be "unreadable could not read string at 0x4 due to protocol error E08 during memory read for packet $m4", empty string or something like at screenshot below:
If don't call http.Get and just return string synchronously or use the variable args after http.Get there won't be a problem, otherwise:
What did you expect to see?
I expect to see via debugger the same value of args before and after method (where it was used) called.
PS It seems the problem happened when I updated golang to the latest version, previously it was 1.24.4 as I remember. Also I tried the same example at Windows laptop and it works fine. I already reported an issue at delve project. They said it probably has to do with compiler optimizations not debugger @aarzilli
Full Environment:
Delve Debugger Version: 1.25.1 Build: $Id: 4e95e55b6b38b12e8509c91ec55261df1f7ee38f Mac Sequoia 15.6.1 arm64 VS Code Version: 1.103.1 (Universal) Go for Visual Studio Code 0.48.0
Comment From: gabyhelp
Related Issues
- net/url: silent Segmentation fault. #51140 (closed)
- The debug result is different with go run result ( using unsafe. Pointer, got []byte cap = 0 and len >0 ) #40541 (closed)
- cmd/compile: Possible stack corruption when passing pointer to a "named return variable" to deferred function #13587 (closed)
- affected/package: net/http #49985 (closed)
- cmd/cgo: opaque struct pointers are broken since Go 1.15.3 #42032 (closed)
- cmd/compile: wrong line number generated after function call #47260 (closed)
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: adonovan
The "unreadable could not read string at %X due to protocol error" message seems like a Delve (debugger) issue.
The fact that the debugger cannot read a string out of args.URL seems like a liveness issue: the compiler may have decided the args variable is no longer needed after the HTTP request and has reused the registers (or stack slot) for another purpose.
Comment From: cherrymui
From the Delve issue https://github.com/go-delve/delve/issues/4092#issuecomment-3210703550 it seems that when the stack slot is no longer live, and the stack got copied, which doesn't update the stack slot as it is dead. This seems a reasonable behavior.
I'm not sure if it is a good idea to keep variables live longer than its lifetime. Lifetime is part of the program's behavior. I don't think the -N flag should change that.
We could arrange the stack copying to adjust stack slots even if it's dead. (We don't reuse a pointer slot for non-pointer values.) Maybe in -N mode the compiler can emit a separate stack map for copying, which includes all stack slots that could possibly be live in the frame. Not sure it's worth it.
cc @golang/compiler
Comment From: randall77
I'm not sure adjusting dead stack slots is the right thing here. Being dead, the thing the pointer points to may have been freed and reused. So there's still no guarantee the debugger can make sense of the value.
Comment From: cherrymui
Good point!
One could imagine a "extra debug" mode that would keep everything alive indefinitely. But that is also a behavior change: besides the resource usage, it also makes finalizers/cleanups never run, which could alter the program's behavior.
Perhaps this is just infeasible.
Comment From: prattmic
I'm not sure if this is a DWARF or delve issue, but I would expect such a variable to say "optimized out" or similar in gdb once it is no longer live. It seems like that should happen here, rather than printing garbage?
Comment From: randall77
There is a tension here. It is often useful in a debugger to see values of variables after they are dead.
n = ...
f(n)
-- breakpoint here --
It's always nice to be able to print n at this point, even if it is dead. (That's one benefit of -N. Without it, n might not be available anywhere.)
The tricky part is when n has a pointer in it. Maybe we could tell dlv "use at your own risk". Not sure how actionable such a thing might be. It would possibly be useful to say something like "string of length 5, but contents are not available". Probably there is a way to say that in dwarf, if dlv could make use if it.
We do similar best-effort things in tracebacks. Maybe we're printing old or invalid arguments. But that is often better than no arguments at all. And we use "?" to mean "use at your own risk".
Comment From: adonovan
I agree with @prattmic and think we should reopen an issue. If the variable is no longer live, then the DWARF information should record that, and the user should get a sensible error rather than garbage data.
Comment From: SlavaUtesinov
It seems something was changed between 1.24.4 and 1.25.0 versions, because I hadn't noticed such behaviour before, it had worked fine. Also there is no problem at Windows laptop.
Comment From: aarzilli
There are ways to say "this variable is no longer available" (at the expense of debug info size) but AFAIK there is no (standard) way to say "this variable is no longer available but you can check this location at your own risk" (which as @randall77 says, is often useful).