Why Your Go Code Crashes and How to Fix It Without a Headache
\n\nYou 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\nGo 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.
Technique #1: Smart Print — When log.Fatal Isn't Needed
\n\nYes, 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.
Moving from Println to Structured Logging
\n\nInstead of plastering dozens of prints, use the log or slog package (available since Go 1.21):
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\nThis approach gives you structured logs that are easy to parse, filter, and analyze. In production, this will save you from late-night calls.
\n\nLife Hack: Conditional Compilation for Debug Logs
\n\nTo 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\nRun with the -tags debug flag — get detailed logs. Without the flag — clean production.
Technique #2: Delve — Your Personal Detective for Go
\n\nIf 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\ngo install github.com/go-delve/delve/cmd/dlv@latest\n\n\nBreakpoints That Can Think
\n\nDelve 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\nThe debugger will only stop when userID equals 42. Imagine how much time this saves when debugging loops.
Exploring Goroutines
\n\nThe 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\nThe asterisk shows the current goroutine. Switch between them:
\n\n(dlv) goroutine 2\n(dlv) stack\n\n\nWorking with Memory: Seeing What's Inside
\n\nThe locals command shows all local variables, and print displays their values with types:
(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\nTechnique #3: Profiling with pprof — Finding Bottlenecks
\n\nYour 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