Debugging Go Code: From Novice to Pro in One Evening

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Your Go Code Crashes and How to Fix It Without a Headache

\n\n

You wrote what you think is perfect Go code. Compilation went smoothly, tests are green... but in production, everything goes to hell. Sound familiar? Welcome to the real world of development, where bugs aren't just beginner mistakes but part of professional growth. Today, we'll break down Go code debugging so that you not only find errors but prevent them from appearing during the writing stage.

\n\n

Go is a language that gives developers powerful debugging tools right "out of the box." But many only use fmt.Println and are genuinely surprised why their goroutines behave like cats on a hot tin roof. Let's fix this once and for all.

\n\n

Technique #1: Smart Print — When log.Fatal Isn't Needed

\n\n

Yes, I know what you're going to say: "Why do I need delve when I can just print a variable?" And you'd be right 80% of the time. But the problem isn't fmt.Println, it's how you use it.

\n\n

Moving from Println to Structured Logging

\n\n

Instead of plastering dozens of prints, use the log or slog package (available since Go 1.21):

\n\n
package main\n\nimport (\n    "log/slog"\n    "os"\n)\n\nfunc main() {\n    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n    \n    // Instead of fmt.Println("error:", err)\n    if err := doSomething(); err != nil {\n        logger.Error("operation failed", \n            "error", err,\n            "module", "data_processor",\n            "attempt", 3,\n        )\n    }\n}\n
\n\n

This approach gives you structured logs that are easy to parse, filter, and analyze. In production, this will save you from late-night calls.

\n\n

Life Hack: Conditional Compilation for Debug Logs

\n\n

To avoid cluttering production, use build tags:

\n\n
//go:build debug\n\npackage main\n\nvar debug = true\n
\n\n
//go:build !debug\n\npackage main\n\nvar debug = false\n
\n\n

Run with the -tags debug flag — get detailed logs. Without the flag — clean production.

\n\n

Technique #2: Delve — Your Personal Detective for Go

\n\n

If print debugging is a bicycle, then Delve is a Tesla. This debugger is built specifically for Go and understands goroutines, interfaces, and even generics. Installation is simple:

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

Breakpoints That Can Think

\n\n

Delve allows you to set not just breakpoints, but conditional breakpoints:

\n\n
(dlv) break main.go:25 if userID == 42\n(dlv) continue\n
\n\n

The debugger will only stop when userID equals 42. Imagine how much time this saves when debugging loops.

\n\n

Exploring Goroutines

\n\n

The scariest thing in Go is deadlocks and race conditions. Delve shows all active goroutines:

\n\n
(dlv) goroutines\n  Goroutine 1 - User: ./main.go:10 main.main (0x109a5b6)\n  Goroutine 2 - User: ./main.go:15 main.worker (0x109a6c8)\n* Goroutine 3 - User: ./main.go:22 main.worker (0x109a6c8)\n
\n\n

The asterisk shows the current goroutine. Switch between them:

\n\n
(dlv) goroutine 2\n(dlv) stack\n
\n\n

Working with Memory: Seeing What's Inside

\n\n

The locals command shows all local variables, and print displays their values with types:

\n\n
(dlv) locals\ndata = []int len: 100000, cap: 100000\nresult = map[string]int ["error": 5, "success": 99995]\n(dlv) print data[0:5]\n[]int len: 5, cap: 100000, [42, 17, 89, 3, 256]\n
\n\n

Technique #3: Profiling with pprof — Finding Bottlenecks

\n\n

Your code works, but is it slow? pprof is Go's built-in profiler. It will show you where the program "lags" and where it consumes memory.

\n\n

Adding an HTTP Endpoint

Blogs

Book Recommendations