Skip to content

Commit

Permalink
go delve - The Golang Debugger (#825)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* PR feedback

* Adding header

* typo
  • Loading branch information
Adam Gordon Bell authored Apr 19, 2024
1 parent 464ec77 commit 93c8713
Show file tree
Hide file tree
Showing 3 changed files with 367 additions and 0 deletions.
367 changes: 367 additions & 0 deletions blog/_posts/2024-04-18-golang-dlv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
---
title: "go delve - The Golang Debugger "
categories:
- Tutorials
toc: true
author: Adam

internal-links:
- just an example
---
Delve (`dlv`) is a CLI-based debugger for Go, tailored to the language's concurrency model and runtime. It allows you to set breakpoints, inspect goroutines, and evaluate and manipulate variables in real-time. Delve supports remote debugging and seamlessly integrates with major IDEs, including Visual Studio Code. Let me walk you through using it, but first, some background.

## Background

In my recent foray into Python, one thing that has become endlessly useful is the ability to add a `breakpoint()` to a line of code without an IDE or any setup at all, ending up in a console-based debugger at that point.

In the past, I had always associated debuggers with IDEs like Visual Studio or IntelliJ. Those are great. However, I find myself in VS Code most of the time now, and I've never fully understood its debugging support. Instead, I resort to using prints, which usually suffices. But there are times when I need to step through some code and require an easy-to-use debugger. This is where Delve shines, making it a breeze to debug a go executable, a go test, and even, in theory, a go docker container running on a remote host.

## Installing Delve

To install just run `go install` with the package path:

~~~
$ go install github.com/go-delve/delve/cmd/dlv@latest
go: downloading github.com/go-delve/delve v1.22.1
go: downloading github.com/hashicorp/golang-lru v1.0.2
go: downloading github.com/cosiner/argv v0.1.0
go: downloading github.com/derekparker/trie v0.0.0-20230829180723-39f4de51ef7d
go: downloading github.com/go-delve/liner v1.2.3-0.20231231155935-4726ab1d7f62
go: downloading golang.org/x/arch v0.6.0
go: downloading github.com/google/go-dap v0.11.0
go: downloading go.starlark.net v0.0.0-20231101134539-556fd59b42f6
go: downloading github.com/mattn/go-runewidth v0.0.13
go: downloading github.com/rivo/uniseg v0.2.0
~~~

That will install it into your GOPATH:

~~~
$ go env GOPATH
/Users/adam/go
~~~

Make sure you have GOPATH in your path ( I didn't):

~~~
echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.zshrc && source ~/.zshrc
~~~

Then you can run it:

~~~
$ dlv version
Delve Debugger
Version: 1.22.1
Build: $Id: 0c3470054da6feac5f6dcf3e3e5144a64f7a9a48 $
~~~

### Debugging `dlv debug`

Now before `dlv`, I was using `go run` with a lot of parameters to run Earthly:

~~~
go run cmd/earthly/main.go -P -i --buildkit-image earthly/buildkitd:prerelease ./tests/raw-output+gha
~~~

To debug with delve I just change `go run` to `dlv debug` and introduce a `--` to delimit where my apps params start.

~~~
dlv debug cmd/earthly/main.go -- -P -i --buildkit-image earthly/buildkitd:prerelease ./tests/raw-output+gha
~~~

Now, I want to debug this header printing code in Earthly:

~~~{.go caption="conslogging/conslogging.go"}
func (cl ConsoleLogger) PrintPhaseHeader(phase string, disabled bool, special string) {
w := new(bytes.Buffer)
cl.mu.Lock()
defer func() {
_, _ = w.WriteTo(cl.errW)
cl.mu.Unlock()
}()
msg := phase
c := cl.color(phaseColor)
if disabled {
c = cl.color(disabledPhaseColor)
msg = fmt.Sprintf("%s (disabled)", msg)
} else if special != "" {
c = cl.color(specialPhaseColor)
msg = fmt.Sprintf("%s (%s)", msg, special)
}
underlineLength := utf8.RuneCountInString(msg) + 2
if underlineLength < barWidth {
underlineLength = barWidth
}
c.Fprintf(w, " %s", msg)
fmt.Fprintf(w, "\n")
c.Fprintf(w, "%s", strings.Repeat("", underlineLength))
fmt.Fprintf(w, "\n\n")
}
~~~

I want to understand more about the underlineLength calculation at run time. To do so, I set a breakpoint and continue execution until that point is hit:

~~~
(dlv) break conslogging/conslogging.go:230
Breakpoint 1 set at 0x1050c4e44 for github.com/earthly/earthly/conslogging.ConsoleLogger.PrintPhaseHeader() ./conslogging/conslogging.go:230
(dlv) continue
~~~

`dlv` returns this, when I hit a break point:

~~~
225: }
226: underlineLength := utf8.RuneCountInString(msg) + 2
227: if underlineLength < barWidth {
228: underlineLength = barWidth
229: }
=> 230: c.Fprintf(w, " %s", msg)
231: fmt.Fprintf(w, "\n")
232: c.Fprintf(w, "%s", strings.Repeat("—", underlineLength))
233: fmt.Fprintf(w, "\n\n")
234: }
235:
~~~

To check the value of underline length I can use print (p) or list all locals with `locals`

~~~
(dlv) p underlineLength
80
~~~

~~~
(dlv) locals
w = (*bytes.Buffer)(0x1400041c420)
msg = "Init 🚀"
c = ("*github.com/fatih/color.Color")(0x140001e49e0)
underlineLength = 80
(dlv) p underlineLength
~~~

I can also use `args` to get the arguments passed into my function:

~~~
(dlv) args
cl = github.com/earthly/earthly/conslogging.ConsoleLogger {prefix: "", metadataMode: false, isLocal: false,...+14 more}
phase = "Init 🚀"
disabled = false
special = ""
~~~

From this point, I can do many things, including saving a checkpoint that lets me return to this point of execution later, but for now, I just want to modify `underlineLength` and see how that changes the execution. I can do this using `set` and print `p`.

~~~
(dlv) set underlineLength = 5
(dlv) p underlineLength
5
~~~

I can then use next (`n`) to step through the code and continue (`c`) to continue execution and see my new shortened underline. `c` works just like you'd expect continue to work, it continues the execution until another break point is hit.

~~~
(dlv) c
Init 🚀
-----
~~~

That `----` is my much shortened underscore line, but the problem is this function gets called in a loop and so I quickly end up back at my breakpoint again:

~~~
225: }
226: underlineLength := utf8.RuneCountInString(msg) + 2
227: if underlineLength < barWidth {
228: underlineLength = barWidth
229: }
=> 230: c.Fprintf(w, " %s", msg)
231: fmt.Fprintf(w, "\n")
232: c.Fprintf(w, "%s", strings.Repeat("—", underlineLength))
233: fmt.Fprintf(w, "\n\n")
234: }
235:
~~~

I can keep mashing `c` until I get past this, or clear my breakpoint with `clear`:

~~~
breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x1048551ac,0x10483e3c0,0x10483e480 for (multiple functions)() <multiple locations>:0 (0)
Breakpoint unrecoverable-panic (enabled) at 0x10483e750 for runtime.fatalpanic() /opt/homebrew/Cellar/go/1.21.1/libexec/src/runtime/panic.go:1188 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x1050c4e44 for github.com/earthly/earthly/conslogging.ConsoleLogger.PrintPhaseHeader() ./conslogging/conslogging.go:230 (1)
~~~

My breakpoint is breakpoint 1, so I can clear it and continue:

~~~
(dlv) clear 1
(dlv) continue
~~~

**Side Note:** Breakpoints `runtime-fatal-throw` and `unrecoverable-panic` do exactly what they sound like. Delve includes breakpoints on these critical failure points to debug a fatal runtime throw or an unrecoverable panic easily.

With the breakpoint cleared, Earthly now runs to completion.

~~~
Process 91365 has exited with status 0
Process 91365 has exited with status 0
(dlv)
~~~

From that point, I can restart to rerun the same process with the same arguments or quit. And that is quick tutorial on `dlv`.

## Quick Tips

For all the in and outs of `dlv` checkout [the docs](https://github.com/go-delve/delve), but here is some quick tips.

| **Task** | **Go Command** | **Delve Command** |
|--------------------------|-------------------------|----------------------------|
| **Run Program** | `go run main.go` | `dlv debug` |
| **Run Tests** | `go test` | `dlv test` |
| **Compile and Run Executable** | `go build -o myapp && ./myapp` | `dlv exec ./myapp` |
| **Run Specific Test** | `go test -run TestName` | `dlv test -- -test.run TestName` |

### Keyboard Shortcuts

| **Command Name** | **Shortcut** | **Description** |
|------------------|--------------|------------------------------------------------|
| `break` | `b` | Sets a breakpoint at a specific source location. |
| `continue` | `c` | Resumes program execution until a breakpoint. |
| `step` | `s` | Executes the current line, entering functions. |
| `next` | `n` | Executes the current line, skipping functions. |
| `print` | `p` | Prints the value of a variable or expression. |
| `list` | `l` | Displays source around the current execution point. |
| `clear` | | Removes a breakpoint. |
| `set` | | Modifies the value of a variable during debugging. |

## Headless for remote or Containerize Debugging

Delve can be run headless, `--headless` , and then connected to remotely. This can be useful in a docker container where the trick is to install `dlv` and then run it and open the correct port:

~~~{.dockerfile caption="Dockerfile"}
FROM golang:1.18

RUN go install github.com/go-delve/delve/cmd/dlv@latest

COPY myapp .
EXPOSE 40000

# Command to run Delve server
CMD ["dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./myapp"]
~~~

Your app needs to be built with debug capabilities, and of course, you can use Earthy to do this building and containerizing. Here is an Earthfile for building the app with debug symbols and building the debug container:

~~~{.dockerfile caption="Earthfile"}
VERSION 0.8
FROM golang:1.22

build-debug:
WORKDIR /myapp
COPY ./src .
RUN go build -gcflags="all=-N -l" -o /myapp/myapp .
SAVE ARTIFACT /myapp/myapp myapp

containerize-debug:
FROM golang:1.18
# Install Delve
RUN go install github.com/go-delve/delve/cmd/dlv@latest
# Copy the built app from the previous stage
COPY +build-debug/myapp /myapp/myapp
WORKDIR /myapp
EXPOSE 40000
CMD ["dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/myapp/myapp"]
SAVE IMAGE myapp-debug
~~~

Then you can debug like this:

~~~
earthly +containerize-debug
docker run -d -p 40000:40000 myapp-debug
dlv connect localhost:40000
~~~

You could even run delve from right within Earthly:

~~~{.dockerfile caption="Earthfile"}
interactive-debug:
FROM golang:1.18
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY +build-debug/myapp /myapp/myapp

# Did you know earthly also allows you to run commands that require a tty?
RUN --interactive dlv exec /myapp/myapp
~~~

## VS Code and GoLand

Delve is what powers the debugger you see in various go IDEs. If you want to debug go code in VS Code, then the official go extension will probably just work once you configure a launch.js.

For me, I had to add this under `Run`-> `Add Configuration`:

~~~{.yaml caption="launch.js"}
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Earthly",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "./cmd/earthly/main.go",
"args": [
"./examples/c+deps"
],
"env": {
"FORCE_COLOR": "1"
},
"cwd": "${workspaceFolder}"
}
]
}
~~~

This is the equivalent of `dlv debug`. To Debug an executable in VS Code, build with debug symbols `-gcflags="all=-N -l"` and then use `mode=exec`, which corresponds to `dlv exec`:

~~~{.yaml caption="launch.js"}
{
"name": "Debug Earthly Binary",
"type": "go",
"request": "launch",
"mode": "exec",
"env": {
"FORCE_COLOR": "1"
},
"program": "./build/darwin/amd64/earthly",
"args": [
"./examples/c+deps"
],
"cwd": "${workspaceFolder}"
},
~~~

You can make similar changes to `mode` to run tests with `dlv test` and presumably to connect to remote or headless debug sessions.

### GoLand and IntelliJ

Delve works out of the box with GoLand, and with the IntelliJ Go Plugin. It works just like IntelliJ users would expect a debugger to work.

To setup remote debugging a bit more configuration is required but the folks at JetBrains kindly provide a GUI with instructions embedded in it.

<div class="wide">
{% picture content-wide-nocrop {{site.pimages}}{{page.slug}}/7180.png --alt {{ GoLand Remote Go Debugging }} %}
<figcaption>GoLand Remote Go Debugging ( find this under Run&Debug Configurations -> Add Configuration -> Go Remote)</figcaption>
</div>

## That's a Wrap

Ok, that's a wrap. `dlv` can integrate with neovim, vim, neovim, Sublime and probably a number of other editors as well, but setting those up is left as an exercise for the reader. Check out [github](https://github.com/go-delve/delve) for more information on `dlv`, including trace points and checkpoints which are valuable tools that deserve their own write up.

{% include_html cta/bottom-cta.html %}
Binary file added blog/assets/images/golang-dlv/7180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added blog/assets/images/golang-dlv/header.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 93c8713

Please sign in to comment.