Introduction: Why Go is Simple but Not Easy
\n\nThe Go language (Golang) is often praised for its simplicity and conciseness. However, this very simplicity often trips up beginners coming from more "verbose" languages like Python or Java. In this article, we have compiled the top mistakes of beginner Go developers — from typical problems with error handling to non-obvious pitfalls when working with goroutines. If you are just starting your journey in the world of Golang, this selection will help you avoid stepping on the same rake as many before you.
\n\n1. Ignoring Errors and Incorrect Handling
\n\nThe most common mistake in Go is ignoring returned errors. Unlike exceptions in Python or Java, Go uses explicit error checking via a return value. Beginners often write:
\n\npackage main\n\nimport (\n "fmt"\n "os"\n)\n\nfunc main() {\n // ❌ ERROR: ignoring the error\n file, _ := os.Open("file.txt")\n fmt.Println(file.Name())\n}\n\n\nHere, the _ (blank identifier) literally tells the compiler: "I don't care about the error." In a real project, this will lead to a panic or incorrect behavior. The correct approach is to always check the error:
package main\n\nimport (\n "fmt"\n "os"\n)\n\nfunc main() {\n file, err := os.Open("file.txt")\n if err != nil {\n // ✅ Handle the error\n fmt.Println("Error opening file:", err)\n return\n }\n defer file.Close()\n fmt.Println("File opened successfully:", file.Name())\n}\n\n\nTip: Never use _ for errors unless you are 100% sure the error is impossible. It's better to write an extra line if err != nil than to hunt for a bug the night before a deadline.
2. Problems with Goroutines: Data Races and Leaks
\n\nGo is famous for its simplicity in working with concurrency, but this also creates pitfalls. Beginners often forget about synchronizing access to shared data.
\n\n2.1. Data Race
\n\nImagine several goroutines writing to the same variable simultaneously:
\n\npackage main\n\nimport (\n "fmt"\n "sync"\n)\n\nfunc main() {\n counter := 0\n var wg sync.WaitGroup\n\n for i := 0; i < 1000; i++ {\n wg.Add(1)\n go func() {\n // ❌ ERROR: data race\n counter++\n wg.Done()\n }()\n }\n wg.Wait()\n fmt.Println("Counter:", counter) // Result is unpredictable\n}\n\n\nThe result will be different on each run. Solution: use a mutex or atomic operations:
\n\npackage main\n\nimport (\n "fmt"\n "sync"\n "sync/atomic"\n)\n\nfunc main() {\n var counter int64\n var wg sync.WaitGroup\n\n for i := 0; i < 1000; i++ {\n wg.Add(1)\n go func() {\n // ✅ Use atomic addition\n atomic.AddInt64(&counter, 1)\n wg.Done()\n }()\n }\n wg.Wait()\n fmt.Println("Counter:", counter) // Always 1000\n}\n\n\n2.2. Goroutine Leak
\n\nAnother classic mistake is to start a goroutine and forget about it. If a goroutine is waiting for data from a channel that will never be closed, it will hang forever, consuming memory:
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc worker(ch chan int) {\n for msg := range ch {\n fmt.Println(msg)\n }\n}\n\nfunc main() {\n ch := make(chan int)\n go worker(ch) // ❌ ERROR: goroutine will never finish\n ch <- 1\n ch <- 2\n // Channel not closed -> leak\n time.Sleep(time.Second)\n}\n\n\nSolution: always close the channel when you are done sending data, or use a context for cancellation.
\n\n3. Incorrect Use of nil Slices and Maps
\n\nIn Go, n