Top mistakes of beginner Go developers: how to avoid them
Go (or Golang) is a compiled, statically typed programming language created at Google. It has gained popularity due to its simplicity, performance, and built-in support for concurrency. However, even with such a concise syntax, beginners often make the same mistakes. In this article, we will analyze the top 4 typical mistakes of beginner Go developers and show how to avoid them.
1. Ignoring returned errors
One of the most common mistakes is ignoring errors returned by functions. Go has no exceptions (try-catch), and errors are ordinary values that need to be handled explicitly. Many beginners, accustomed to other languages, simply assign the result to a variable, forgetting to check err.
Incorrect:
package main
import ( "fmt" "os")
func main() { file, _ := os.Open("config.txt") // Ignoring the error fmt.Println(file.Name())}If the file does not exist, the program will panic or work with a nil pointer, leading to unexpected behavior.
Correct:
package main
import ( "fmt" "log" "os")
func main() { file, err := os.Open("config.txt") if err != nil { log.Fatalf("Failed to open file: %v", err) } defer file.Close() fmt.Println(file.Name())}Tip: Always check err immediately after calling a function. Use log.Fatal or return the error up the stack, but never ignore it.
2. Incorrect work with goroutines and data races
Goroutines are lightweight threads in Go. However, beginners often launch goroutines without caring about synchronizing access to shared data. This leads to data races, where multiple goroutines simultaneously read and write to the same variable.
Incorrect:
package main
import ( "fmt" "sync")
func main() { counter := 0 var wg sync.WaitGroup
for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // Data race! }() } wg.Wait() fmt.Println(counter) // Result can be anything (not 1000)}Correct (using a mutex):
package main
import ( "fmt" "sync")
type Counter struct { mu sync.Mutex value int}
func (c *Counter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.value++}
func (c *Counter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.value}
func main() { counter := &Counter{} var wg sync.WaitGroup
for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Println(counter.Value()) // Always 1000}Tip: Use mutexes (sync.Mutex) or channels for synchronization. Run tests with the -race flag: go test -race.
3. Confusion between nil slice and empty slice
In Go, slices can be nil or empty. Beginners often do not understand the difference, which leads to errors during serialization (JSON) or passing slices to functions.
- Nil slice: declared as
var s []int. It has no underlying array. - Empty slice: created as
s := []int{}ormake([]int, 0). It has an underlying array, but the length is 0.
Example of confusion:
package main
import ( "e