Skip to content

Go Debugging Guide

A comprehensive guide to debugging Go programs, covering both Delve (recommended) and GDB.


  1. Delve - The Go Debugger (Recommended)
  2. GDB - GNU Debugger (Alternative)
  3. Print Debugging
  4. Common Debugging Scenarios

Delve (dlv) is the recommended debugger for Go programs. It’s specifically designed for Go and provides excellent support for:

  • Goroutines
  • Go runtime internals
  • Go-specific data types
  • Concurrent code debugging

macOS:

Terminal window
brew install delve

Linux:

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

Windows:

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

Verify installation:

Terminal window
dlv version

Option 1: Debug directly

Terminal window
dlv debug main.go

Option 2: Debug with arguments

Terminal window
dlv debug main.go -- arg1 arg2

Option 3: Attach to running process

Terminal window
# Get process ID
ps aux | grep myprogram
# Attach to process
dlv attach <PID>

Option 4: Debug a test

Terminal window
dlv test ./path/to/package

Option 5: Debug a compiled binary

Terminal window
# Compile with debugging info
go build -gcflags="all=-N -l" -o myapp main.go
# Debug the binary
dlv exec ./myapp

Terminal window
# Start debugger
dlv debug main.go
# Inside dlv prompt:
(dlv) break main.main
Breakpoint 1 set at 0x... for main.main() ./main.go:10
# Break at specific line
(dlv) break main.go:15
Breakpoint 2 set at 0x... for main.processData() ./main.go:15
# Break at function in package
(dlv) break fmt.Println
Breakpoint 3 set at 0x... for fmt.Println()
# List all breakpoints
(dlv) breakpoints
Breakpoint 1 at 0x... for main.main() ./main.go:10
Breakpoint 2 at 0x... for main.processData() ./main.go:15
# Clear breakpoint
(dlv) clear 2
# Clear all breakpoints
(dlv) clearall

Example program to debug:

main.go
package main
import "fmt"
func calculate(x, y int) int {
result := x + y
return result * 2
}
func main() {
a := 5
b := 10
sum := calculate(a, b)
fmt.Println("Result:", sum)
}

Debugging session:

Terminal window
$ dlv debug main.go
Type 'help' for list of commands.
# Set breakpoint at main
(dlv) break main.main
Breakpoint 1 set at 0x10a4780 for main.main() ./main.go:11
# Run program
(dlv) continue
> main.main() ./main.go:11 (hits breakpoint[1])
# Show current code
(dlv) list
> 11: func main() {
12: a := 5
13: b := 10
14: sum := calculate(a, b)
15: fmt.Println("Result:", sum)
16: }
# Step to next line (stays in current function)
(dlv) next
> main.main() ./main.go:12
(dlv) next
> main.main() ./main.go:13
# Print variable value
(dlv) print a
5
# Step into function call
(dlv) step
> main.calculate() ./main.go:6
# Show where we are
(dlv) list
> 6: func calculate(x, y int) int {
7: result := x + y
8: return result * 2
9: }
# Print function arguments
(dlv) args
x = 5
y = 10
# Continue to next line
(dlv) next
> main.calculate() ./main.go:7
# Print local variables
(dlv) locals
result = 842288255632 (uninitialized)
# Execute next line
(dlv) next
> main.calculate() ./main.go:8
# Check result after calculation
(dlv) print result
15
# Step out of current function
(dlv) stepout
> main.main() ./main.go:14
# Print return value
(dlv) print sum
30
# Continue to end
(dlv) continue
Result: 30
Process exited with status 0

Terminal window
# Print variable
(dlv) print myVar
# Print with full type information
(dlv) print -v myVar
# Print pointer dereference
(dlv) print *myPointer
# Print struct fields
(dlv) print myStruct.fieldName
# Print slice/array elements
(dlv) print mySlice[0]
# Print all local variables
(dlv) locals
# Print all function arguments
(dlv) args
# Print all variables (locals + args)
(dlv) vars
# Set variable value
(dlv) set myVar = 42
# Call function
(dlv) call myFunction(arg1, arg2)

Example with goroutines:

concurrent.go
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d processing job %d\n", id, j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 5; a++ {
<-results
}
}

Debug goroutines:

Terminal window
$ dlv debug concurrent.go
# Set breakpoint in worker function
(dlv) break worker
Breakpoint 1 set at 0x... for main.worker() ./concurrent.go:9
# Run
(dlv) continue
# List all goroutines
(dlv) goroutines
* Goroutine 1 - User: ./concurrent.go:26 main.main (0x10a4980)
Goroutine 2 - User: ./concurrent.go:10 main.worker (0x10a4820)
Goroutine 3 - User: ./concurrent.go:10 main.worker (0x10a4820)
Goroutine 4 - User: ./concurrent.go:10 main.worker (0x10a4820)
# Switch to specific goroutine
(dlv) goroutine 2
Switched to goroutine 2
# Print goroutine-local variables
(dlv) locals
id = 1
j = 1
# View stack trace for current goroutine
(dlv) stack
# View stack trace for all goroutines
(dlv) goroutines -t
# Continue all goroutines
(dlv) continue

Terminal window
# Break only when condition is true
(dlv) break main.go:15
(dlv) condition 1 myVar > 100
# Break with hit count
(dlv) break main.go:20
(dlv) condition 2 count == 5
# Clear condition
(dlv) condition 1 -clear
# Break with complex condition
(dlv) break processData
(dlv) condition 3 len(data) > 0 && data[0] == "test"

Terminal window
# Show source around current line
(dlv) list
(dlv) ls # short form
(dlv) list 50 # show line 50
# Show stack trace
(dlv) stack
(dlv) bt # short form
# Move up/down stack frames
(dlv) up
(dlv) down
(dlv) frame 2 # jump to specific frame
# Restart program
(dlv) restart
(dlv) r # short form
# Exit debugger
(dlv) exit
(dlv) quit
(dlv) q # short form
# Help
(dlv) help
(dlv) help break # help for specific command

Install Go extension:

  1. Open VS Code
  2. Install “Go” extension by Go Team at Google

Debug configuration (.vscode/launch.json):

{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}",
"args": [],
"showLog": true
},
{
"name": "Launch File",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${file}"
},
{
"name": "Attach to Process",
"type": "go",
"request": "attach",
"mode": "local",
"processId": 0
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}"
}
]
}

Using VS Code debugger:

  1. Set breakpoints by clicking left of line numbers
  2. Press F5 to start debugging
  3. Use debug toolbar:
    • Continue (F5)
    • Step Over (F10)
    • Step Into (F11)
    • Step Out (Shift+F11)
    • Restart (Ctrl+Shift+F5)
    • Stop (Shift+F5)
  4. Inspect variables in “Variables” panel
  5. View call stack in “Call Stack” panel
  6. Watch expressions in “Watch” panel

⚠️ Warning: GDB support for Go exists but is limited. Delve is strongly recommended for Go debugging. GDB can be difficult to set up, especially on macOS, and lacks proper support for:

  • Goroutines
  • Go-specific data types
  • Go runtime internals

Use GDB only if Delve is unavailable.


Terminal window
# Disable optimization and inlining
go build -gcflags="-N -l" -o myapp main.go

Flags explained:

  • -N: Disable optimizations
  • -l: Disable inlining
Terminal window
gdb myapp

Step 3: Load Go Runtime Support (Optional)

Section titled “Step 3: Load Go Runtime Support (Optional)”

Inside GDB:

(gdb) source $GOROOT/src/runtime/runtime-gdb.py

If this fails, find the correct path:

Terminal window
# Find Go installation
go env GOROOT
# Use full path
(gdb) source /usr/local/go/src/runtime/runtime-gdb.py

On macOS with Homebrew Go:

(gdb) source /usr/local/opt/go/libexec/src/runtime/runtime-gdb.py

Example debugging session:

Terminal window
$ gdb myapp
GNU gdb (GDB) 12.1
...
# Set breakpoint
(gdb) break main.main
Breakpoint 1 at 0x...
# Set breakpoint at line number
(gdb) break main.go:22
Breakpoint 2 at 0x...
# Run program
(gdb) run
# Run with arguments
(gdb) run arg1 arg2
# List source code
(gdb) list
# Print variable
(gdb) print myVar
# Print with type info
(gdb) whatis myVar
# Step to next line (skip over functions)
(gdb) next
(gdb) n # short form
# Step into function
(gdb) step
(gdb) s # short form
# Continue execution
(gdb) continue
(gdb) c # short form
# View local variables
(gdb) info locals
# View stack trace
(gdb) backtrace
(gdb) bt # short form
# Quit GDB
(gdb) quit
(gdb) q # short form

CommandShortDescription
runrStart program execution
break <location>bSet breakpoint
continuecResume execution
nextnStep over (next line)
stepsStep into function
finishfinStep out of function
listlShow source code
print <var>pPrint variable value
info localsShow local variables
info argsShow function arguments
backtracebtShow call stack
frame <n>fSelect stack frame
delete <n>dDelete breakpoint
quitqExit GDB

  1. Poor goroutine support: Cannot easily inspect or switch between goroutines
  2. Go runtime complexity: GDB struggles with Go’s runtime internals
  3. Type system: Limited understanding of Go-specific types (channels, interfaces, etc.)
  4. macOS issues: Requires code signing and special setup on macOS
  5. Optimization: Go compiler optimizations can confuse GDB

Recommendation: Use Delve instead of GDB for Go programs.


Sometimes simple print statements are the fastest way to debug.

package main
import "fmt"
func calculate(x, y int) int {
fmt.Printf("DEBUG: calculate called with x=%d, y=%d\n", x, y)
result := x + y
fmt.Printf("DEBUG: result before multiply=%d\n", result)
result = result * 2
fmt.Printf("DEBUG: final result=%d\n", result)
return result
}
func main() {
sum := calculate(5, 10)
fmt.Println("Result:", sum)
}
package main
import (
"log"
)
func init() {
// Add file and line numbers to log output
log.SetFlags(log.Lshortfile | log.LstdFlags)
}
func processData(data []string) {
log.Printf("Processing %d items", len(data))
for i, item := range data {
log.Printf("Item %d: %s", i, item)
}
}
package main
import (
"fmt"
"os"
)
var debug = os.Getenv("DEBUG") == "1"
func debugf(format string, args ...interface{}) {
if debug {
fmt.Printf("[DEBUG] "+format+"\n", args...)
}
}
func main() {
debugf("Starting application")
// Regular code
debugf("Processing complete")
}

Usage:

Terminal window
# Run without debug
go run main.go
# Run with debug output
DEBUG=1 go run main.go

panic.go
package main
func divide(a, b int) int {
return a / b
}
func main() {
result := divide(10, 0) // Will panic
println(result)
}

Debug with Delve:

Terminal window
$ dlv debug panic.go
(dlv) break main.main
(dlv) continue
# Step through to see where panic occurs
(dlv) next
> panic: runtime error: integer divide by zero

race.go
package main
import (
"fmt"
"time"
)
func main() {
counter := 0
// Multiple goroutines modifying same variable
for i := 0; i < 100; i++ {
go func() {
counter++ // Race condition!
}()
}
time.Sleep(time.Second)
fmt.Println("Counter:", counter)
}

Detect with race detector:

Terminal window
# Run with race detector
go run -race race.go
# Output shows race condition:
# WARNING: DATA RACE
# Write at 0x... by goroutine 7:
# main.main.func1()
# race.go:13 +0x...

Fix with mutex:

import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
counter := 0
for i := 0; i < 100; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
time.Sleep(time.Second)
fmt.Println("Counter:", counter)
}

deadlock.go
package main
func main() {
ch := make(chan int)
// Trying to receive from empty unbuffered channel
val := <-ch // Deadlock! No one sends
println(val)
}

Run:

Terminal window
$ go run deadlock.go
fatal error: all goroutines are asleep - deadlock!

Debug with Delve:

Terminal window
$ dlv debug deadlock.go
(dlv) break main.main
(dlv) continue
(dlv) next
# Shows program hanging on channel receive
# Check goroutines
(dlv) goroutines
* Goroutine 1 - User: ./deadlock.go:6 main.main (0x...)
[waiting for channel receive]

server.go
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Set breakpoint here to inspect requests
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}

Debug with Delve:

Terminal window
$ dlv debug server.go
(dlv) break handler
(dlv) continue
# In another terminal, make request:
# curl localhost:8080/world
# Back in debugger:
> main.handler() ./server.go:9
(dlv) print r.URL.Path
"/world"
(dlv) print r.Method
"GET"
(dlv) continue

CommandAction
dlv debugStart debugging
break <location>Set breakpoint
continue / cContinue execution
next / nStep over
step / sStep into
stepoutStep out
print <var> / pPrint variable
localsShow local variables
argsShow function arguments
goroutinesList goroutines
goroutine <id>Switch to goroutine
stack / btShow stack trace
list / lsShow source
restart / rRestart program
exit / quit / qExit debugger
Terminal window
# Disable optimizations for debugging
go build -gcflags="all=-N -l" -o myapp
# Enable race detector
go build -race -o myapp
# Both
go build -race -gcflags="all=-N -l" -o myapp
Terminal window
# Enable debug output
DEBUG=1 go run main.go
# Enable race detector
go run -race main.go
# Set GOMAXPROCS (for concurrency debugging)
GOMAXPROCS=1 go run main.go

  1. Use Delve for Go debugging (not GDB)
  2. Compile with -gcflags="all=-N -l" for better debugging
  3. Use race detector (-race) for concurrent code
  4. Set breakpoints strategically at function entry and key logic points
  5. Inspect goroutines when debugging concurrent code
  6. Use conditional breakpoints to catch specific cases
  7. Integrate with your IDE (VS Code, GoLand) for visual debugging
  8. Keep print debugging as a quick alternative for simple issues
  9. Enable verbose logging in development
  10. Test with -v flag to see detailed test output


Master debugging to write better Go code faster!