Delve is a debugger built specifically for Golang, offering better integration than gdb, and a more powerful alternative to Println-style debugging.

Here’s how to debug a simple web server using Delve.

// hello_gophers.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello Gophers")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Create this source file, and from its directory run dlv debug. This will compile, start, and attach to the program.

$ dlv debug
Type 'help' for list of commands.
(dlv)

Typing help here as the prompt suggests will reveal many useful commands, the most common with aliases.

Next type continue–or c as its alias–to run the program.

Viewing http://localhost:8080/ in a browser (or using curl at the command line) should return the text ‘Hello Gophers.’

Then enter ctrl+c at the dlv prompt to halt execution. Now it’s time to create a breakpoint using the break command (or b for short):

(dlv) break main.handler
Breakpoint 1 set at 0x1359c63 for main.handler() ./hello_gophers.go:9

The output confirms the breakpoint setting, and its location. Enter c again to continue, then refresh the browser at http://localhost:8080/, return to the dlv terminal session and the breakpoint should have been hit:

(dlv) c
> main.handler() ./hello_gophers.go:9 (hits goroutine(6):1 total:1) (PC: 0x1359c63)
     4:	    "fmt"
     5:	    "log"
     6:	    "net/http"
     7:	)
     8:
=>   9:	func handler(w http.ResponseWriter, r *http.Request) {
    10:	    fmt.Fprintf(w, "Hello Gophers")
    11:	}
    12:
    13:	func main() {
    14:	    http.HandleFunc("/", handler)
(dlv)

Commands

With the debugger in this state, a number of different commands may be used:

list

list will show the current source location, presently at the breakpoint. But a function may also be specified–try list main.main for example, to display its source. (Note the package prefix is included with the name.)

funcs

funcs will display all available funcs, so adding a regex search pattern to this will be helpful. funcs ^main for example will display both of this package’s functions. Entering funcs main will also work, but more matches will be returned without the ‘^’ (start of string) character.

continue (c), next (n), step (s), stepout (so)

Used for navigation once the breakpoint is hit, these are common debugger commands explained by help.

This evaluates any expression. At the breakpoint above for example, p r.Header will print the headers sent along with the http.Request passed to handler(). This is very helpful for debugging.

stack (bt)

Prints the stack trace:

(dlv) bt
0  0x0000000001359c63 in main.handler
   at ./hello_gophers.go:9
1  0x000000000131d5a4 in net/http.HandlerFunc.ServeHTTP
   at /usr/local/Cellar/go/1.14/libexec/src/net/http/server.go:2012
2  0x0000000001320226 in net/http.(*ServeMux).ServeHTTP
   at /usr/local/Cellar/go/1.14/libexec/src/net/http/server.go:2387
3  0x000000000132171f in net/http.serverHandler.ServeHTTP
   at /usr/local/Cellar/go/1.14/libexec/src/net/http/server.go:2807
4  0x000000000131c8a6 in net/http.(*conn).serve
   at /usr/local/Cellar/go/1.14/libexec/src/net/http/server.go:1895
5  0x000000000106c151 in runtime.goexit
   at /usr/local/Cellar/go/1.14/libexec/src/runtime/asm_amd64.s:1373
(dlv)

frame

This allows setting the current frame, as displayed by stack: frame 2.

breakpoints (bp)

Display the current breakpoints:

(dlv) breakpoints
Breakpoint 1 at 0x1359c63 for main.handler() ./hello_gophers.go:9 (2)
(dlv)

condition (cond)

This adds a condition to an existing breakpoint, so that execution will only halt based on some criterion. For example: cond 2 r.URL.Path == "/accounts". Note the breakpoint id must be specified with the command.

clear and clearall

Clear a specific breakpoint from the breakpoints list, clear 1, or all breakpoints with clearall.

(dlv) clear 1
Breakpoint 1 cleared at 0x1359c63 for main.handler() ./hello_gophers.go:9
(dlv) breakpoints
(dlv)

exit

To end the session, use the exit command. (If using attach, it includes an option for ending the target process.)

Using Attach

In addition to starting a debugging session with dlv debug, it is also possible to attach to a running process using the command dlv attach (pid), where ‘pid’ is its id. The following steps apply to the example program:

  1. Build the source: go build -o hello_gophers hello_gophers.go
  2. Run: ./hello_gophers
  3. Open a different terminal session/tab, and find the id of the running process: ps aux|grep hello_gophers. In this case, assume the id returned is 17001.
  4. From this other terminal session, attach to the process by specifying the id: dlv attach 17001.
  5. Now the same steps described earlier may be followed: create a break point, run continue to reach this break point, and debug with the above commands.

Closing

This has been a brief introduction to debugging Go using Delve. It would be most beneficial to practice using these commands with a simple example like this one. Delve’s documentation may be found here.