Top 10 Mistakes in Golang and How to Avoid Them (With Examples)

Golang (Go) is known for its simplicity, efficiency, and concurrency support, making it a popular choice for building robust applications. However, like any programming language, common mistakes can lead to inefficiencies, bugs, and performance issues.

In this guide, we will explore the 10 most common mistakes in Go, explain why they happen, and show how to fix them with real-world examples.

Let’s get started! ⚡

1️⃣ Ignoring Errors from Functions

Bad Code (Ignoring Errors)

package main

import "os"

func main() {
    f, _ := os.Open("file.txt") // Ignoring error
    defer f.Close() // ❌ Will panic if file.txt doesn’t exist
}

🔴 Issue: Ignoring errors leads to silent failures and potential panics.

Good Code (Handling Errors Properly)

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("file.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer f.Close()
}

Why is this better?
✔ Ensures graceful error handling.
✔ Prevents unexpected crashes.

2️⃣ Not Using defer Correctly

Bad Code (Using defer Inside Loops)

package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i) // ❌ Executed at function exit, not loop iteration
    }
}

🔴 Issue: Deferred calls execute in Last-In-First-Out (LIFO) order when the function exits.

Good Code (Placing defer Correctly)

package main

import "fmt"

func main() {
    for i := 0; i < 3; i++ {
        func(n int) {
            defer fmt.Println(n) // ✅ Each iteration calls a separate function
        }(i)
    }
}

Why is this better?
✔ Ensures expected execution order.

3️⃣ Using Global Variables Unnecessarily

Bad Code (Using Global Variables)

package main

import "fmt"

var count int

func increment() {
    count++
}

func main() {
    increment()
    fmt.Println(count) // ❌ Modifying global state (not thread-safe)
}

🔴 Issue: Global variables lead to race conditions in concurrent applications.

Good Code (Using Local Variables or Structs)

package main

import "fmt"

type Counter struct {
    count int
}

func (c *Counter) increment() {
    c.count++
}

func main() {
    c := Counter{}
    c.increment()
    fmt.Println(c.count) // ✅ Local scope ensures safety
}

Why is this better?
Avoids race conditions in concurrent programs.

4️⃣ Not Closing Resources

Bad Code (Forgetting to Close Files)

package main

import "os"

func main() {
    f, err := os.Open("file.txt")
    if err != nil {
        return
    }
    // ❌ Forgot to close file, causing a resource leak
}

🔴 Issue: Leads to memory leaks and open file descriptor issues.

Good Code (Using defer f.Close())

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("file.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer f.Close() // ✅ Ensures file is closed at function exit
}

Why is this better?
✔ Prevents resource leaks.

5️⃣ Using Slices Incorrectly

Bad Code (Modifying the Underlying Array)

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3, 4}
    slice := arr[:2]
    slice = append(slice, 5, 6) // ❌ Modifies original array
    fmt.Println(arr) // Unexpected result
}

🔴 Issue: Append can modify the original array unexpectedly.

Good Code (Using copy())

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3, 4}
    slice := make([]int, len(arr[:2]))
    copy(slice, arr[:2])
    slice = append(slice, 5, 6) // ✅ Does not modify original array
    fmt.Println(arr) // Unchanged
}

Why is this better?
✔ Avoids unexpected modifications.

6️⃣ Using Unbuffered Channels in High-Concurrency Applications

Bad Code (Blocking a Goroutine)

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    go func() {
        ch <- 42 // ❌ Deadlock if no receiver
    }()

    fmt.Println(<-ch)
}

🔴 Issue: If no receiver is available, the program deadlocks.

Good Code (Using Buffered Channels)

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    
    ch <- 42 // ✅ Non-blocking send
    fmt.Println(<-ch)
}

Why is this better?
✔ Avoids deadlocks in concurrent applications.

7️⃣ Improper Error Wrapping

Bad Code (Losing Context in Errors)

package main

import (
    "errors"
    "fmt"
)

func readFile() error {
    return errors.New("file not found")
}

func main() {
    err := readFile()
    fmt.Println(err) // ❌ No context on where the error happened
}

🔴 Issue: The error message doesn’t indicate where it came from.

Good Code (Using fmt.Errorf)

package main

import (
    "fmt"
)

func readFile() error {
    return fmt.Errorf("readFile error: %w", fmt.Errorf("file not found"))
}

func main() {
    err := readFile()
    fmt.Println(err) // ✅ Shows where the error originated
}

Why is this better?
✔ Provides clearer error messages.

8️⃣ Misusing Goroutines

Bad Code (Launching Too Many Goroutines)

package main

import "fmt"

func main() {
    for i := 0; i < 1000000; i++ {
        go fmt.Println(i) // ❌ Can cause memory exhaustion
    }
}

🔴 Issue: Spawning too many goroutines exceeds system limits.

Good Code (Using Worker Pools)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    jobs := make(chan int, 10)

    for i := 0; i < 5; i++ { // ✅ Limited workers
        wg.Add(1)
        go func() {
            for job := range jobs {
                fmt.Println(job)
            }
            wg.Done()
        }()
    }

    for j := 0; j < 10; j++ {
        jobs <- j
    }

    close(jobs)
    wg.Wait()
}

Why is this better?
✔ Controls resource usage efficiently.

9️⃣ Not Using context.Context

Bad Code (No Timeout Control)

package main

import (
    "net/http"
)

func fetchData() {
    _, _ = http.Get("https://example.com") // ❌ No timeout
}

🔴 Issue: No timeout control means requests can hang indefinitely.

Good Code (Using context.WithTimeout)

package main

import (
    "context"
    "net/http"
    "time"
)

func fetchData() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
    _, _ = http.DefaultClient.Do(req)
}

Why is this better?
Prevents stuck requests.

🔟 Using panic for Error Handling

Bad Code (Using panic)

package main

func divide(a, b int) int {
    if b == 0 {
        panic("cannot divide by zero") // ❌ Terminates program
    }
    return a / b
}

Good Code (Returning an Error)

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

Why is this better?
✔ Avoids unexpected crashes.

🎯 Conclusion

By avoiding these 10 common Go mistakes, you can write faster, more reliable, and more maintainable Go applications. 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare