Top 20 Go Mistakes

1. Ignoring ctx.Done() → Goroutine leaks

❌ Wrong:

go func() {
    time.Sleep(10 * time.Second) // never stops if request canceled
}()

✅ Fix:

go func(ctx context.Context) {
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("done")
    case <-ctx.Done():
        fmt.Println("canceled:", ctx.Err())
    }
}(r.Context())

Reason: Without cancellation, goroutines survive after client disconnects → memory leaks.


2. Capturing loop variables incorrectly

❌ Wrong:

for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }() // prints 3,3,3
}

✅ Fix:

for i := 0; i < 3; i++ {
    go func(i int) { fmt.Println(i) }(i) // prints 0,1,2
}

Reason: Closures capture references, not values → unintended results.


3. Blocking on unbuffered channels

❌ Wrong:

ch := make(chan int)
ch <- 1 // deadlock, no receiver

✅ Fix:

ch := make(chan int, 1)
ch <- 1

Reason: Unbuffered channels need simultaneous sender/receiver → deadlocks easily.


4. Misusing context as a data bag

❌ Wrong:

goctx = context.WithValue(r.Context(), "user", "iman")

✅ Fix:

func handler(w http.ResponseWriter, r *http.Request, user string) { ... }

Reason: Context is for cancellation/deadlines, not app data → code becomes unclear.


5. Ignoring errors

❌ Wrong:

goval, _ := strconv.Atoi("abc") // fails silently

✅ Fix:

val, err := strconv.Atoi("abc")
if err != nil {
    log.Printf("invalid input: %v", err)
}

Reason: Hidden errors → hard-to-debug production failures.


6. Not closing resources

❌ Wrong:

resp, _ := http.Get("https://example.com")
// leak: resp.Body not closed

✅ Fix:

resp, err := http.Get("https://example.com")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()

Reason: File/socket leaks → "too many open files" in production.


7. Overusing goroutines

❌ Wrong:

for i := 0; i < 1e6; i++ {
    go heavyTask(i)
}

✅ Fix:

sem := make(chan struct{}, 100) // limit concurrency
for i := 0; i < 1e6; i++ {
    sem <- struct{}{}
    go func(i int) {
        defer func(){ <-sem }()
        heavyTask(i)
    }(i)
}

Reason: Goroutines are cheap but not free → uncontrolled spawns cause OOM.


8. Using global state

❌ Wrong:

var db *sql.DB

✅ Fix:

type Service struct { DB *sql.DB }

Reason: Globals hide dependencies, hurt testing, and increase coupling.


9. Race conditions with maps

❌ Wrong:

m := map[string]int{}
go func() { m["x"] = 1 }()

✅ Fix:

var mu sync.Mutex
mu.Lock(); m["x"] = 1; mu.Unlock()

Reason: Maps are not safe for concurrent writes → random crashes.


10. Improper defer in loops

❌ Wrong:

for _, f := range files {
    fh, _ := os.Open(f)
    defer fh.Close()
}

✅ Fix:

for _, f := range files {
    fh, _ := os.Open(f)
    process(fh)
    fh.Close()
}

Reason: Too many open files at once → resource exhaustion.


11. Poor project structure

❌ Wrong: All logic in main.go.

✅ Fix:

textcmd/      → entry points
internal/     → business logic
pkg/          → reusable libs

Reason: Bad structure makes scaling and testing painful.


12. Declaring interfaces in providers

❌ Wrong:

// in db package
type DB interface { Query(string) }

✅ Fix:

// in consumer
type UserRepo interface { GetUser(id int) User }

Reason: Interfaces should belong to consumers → avoids over-abstraction.


13. Error wrapping mistakes

❌ Wrong:

return fmt.Errorf("failed: %v", err)

✅ Fix:

return fmt.Errorf("failed: %w", err)

Reason: %w preserves error chain → usable with errors.Is/As.


14. Misunderstanding slices

❌ Wrong:

a := []int{1,2,3}
b := a
b[0] = 99 // also changes a

✅ Fix:

b := make([]int, len(a))
copy(b, a)

Reason: Slices share backing arrays → mutating one affects others.


15. Nil vs empty slice confusion

❌ Wrong:

var s []string // nil
json.Marshal(s) // outputs "null"

✅ Fix:

s := []string{} // empty slice
json.Marshal(s) // outputs "[]"

Reason: APIs often expect [], not null.


16. Leaking DB connections

❌ Wrong:

rows, _ := db.Query("SELECT * FROM users")
// forget rows.Close()

✅ Fix:

rows, err := db.Query("SELECT * FROM users")
if err != nil { return err }
defer rows.Close()

Reason: Open result sets exhaust DB pool → blocks new queries.


17. Using panics for control flow

❌ Wrong:

if badInput {
    panic("invalid")
}

✅ Fix:

if badInput {
    return fmt.Errorf("invalid input")
}

Reason: Panics crash programs, errors let you recover gracefully.


18. Not profiling (pprof)

❌ Wrong: Guessing where bottlenecks are.

✅ Fix:

import _ "net/http/pprof"
// visit localhost:6060/debug/pprof/

Reason: Guessing wastes time, profiling gives facts.


19. Not testing concurrency (-race)

❌ Wrong: Running tests normally.

✅ Fix:

go test -race ./...

Reason: Race detector finds hidden concurrency bugs early.


20. Ignoring Go idioms

❌ Wrong:

func GetName(u User) string { return u.name } // Java style

✅ Fix:

func (u User) Name() string { return u.name }

Reason: Idiomatic Go is simpler, easier to read, and what teams expect.

Read more