50-Mistake-Golang

50 Common Mistakes in Go Development

Go is a powerful and concise language, but its simplicity can lead to subtle mistakes, especially for developers new to its paradigms. This guide outlines 50 common pitfalls in Go development, with explanations and examples to help you write robust and idiomatic Go code. These mistakes are derived from common errors observed in the Go community, translated and adapted from the referenced source.

1. Left Curly Brace on a New Line

Go’s syntax requires opening curly braces ({) to be on the same line as the statement. A newline before { causes a compilation error.

// Wrong
func main()
{
    fmt.Println("Hello")
}

// Correct
func main() {
    fmt.Println("Hello")
}

2. Unused Variables

Go disallows unused variables to enforce clean code. Declared variables must be used, or the code won’t compile.

// Wrong
func main() {
    x := 10 // Error: x declared but not used
}

// Correct
func main() {
    x := 10
    fmt.Println(x)
}

Use _ for variables you intentionally ignore, like loop indices or return values.

_, err := someFunction()

3. Unused Imports

Unused import statements cause compilation errors. Remove or use imported packages.

// Wrong
import (
    "fmt"
    "log" // Error: imported and not used
)

// Correct
import (
    "fmt"
)

Use goimports to automatically manage imports.

4. Short Variable Declarations Only in Functions

The := operator for short variable declarations is only valid inside functions, not at package level.

// Wrong
package main
x := 10 // Error: non-declaration statement outside function body

// Correct
package main
var x = 10

5. Redeclaring Variables with :=

Short declarations (:=) cannot redeclare variables in the same block if they’re already declared, unless at least one new variable is introduced.

// Wrong
func main() {
    x := 10
    x := 20 // Error: no new variables on left side of :=
}

// Correct
func main() {
    x := 10
    x = 20 // Use assignment
}

6. Accidental Variable Shadowing

Short declarations in inner scopes can shadow outer variables, leading to unexpected behavior.

func main() {
    x := 10
    if true {
        x := 20 // Shadows outer x
        fmt.Println(x) // Prints 20
    }
    fmt.Println(x) // Prints 10
}

Use = to assign to the outer variable or avoid redeclaring.

7. Misusing nil Slices

A nil slice has len=0, cap=0, and can be appended to, but developers may assume it’s invalid.

var s []int // nil slice
s = append(s, 1) // Works fine

Check for nil explicitly only when necessary, as len(s) == 0 is usually sufficient.

8. Appending to a Slice Without Checking Capacity

Appending to a slice may reallocate the underlying array if capacity is exceeded, affecting performance.

s := make([]int, 0, 10)
s = append(s, 1) // Uses preallocated capacity

Preallocate capacity with make when the size is predictable.

9. Modifying Slices Sharing the Same Array

Slices referencing the same underlying array share modifications, which can lead to bugs.

arr := [5]int{0, 1, 2, 3, 4}
s1 := arr[1:3] // [1, 2]
s2 := arr[2:4] // [2, 3]
s1[1] = 99
fmt.Println(s2) // [99, 3]

Use copy to create independent slices.

s3 := make([]int, len(s1))
copy(s3, s1)

10. Incorrect Slice Capacity in Reslicing

Reslicing with s[i:j:k] sets cap = k-i. Misjudging k can limit future appends.

s := []int{1, 2, 3, 4}
s2 := s[1:3:3] // len=2, cap=2
s2 = append(s2, 5) // May reallocate

11. Ignoring Return Values

Go functions often return multiple values (e.g., result and error). Ignoring them can hide issues.

// Wrong
func main() {
    os.Open("file.txt") // Ignores error
}

// Correct
func main() {
    _, err := os.Open("file.txt")
    if err != nil {
        log.Fatal(err)
    }
}

12. Misusing defer

The defer statement delays execution until the surrounding function returns, but arguments are evaluated immediately.

func main() {
    i := 1
    defer fmt.Println(i) // Prints 1, not 2
    i = 2
}

Use a closure to capture the current state.

defer func() { fmt.Println(i) }()

13. Multiple defer Order

Deferred functions execute in LIFO (last-in, first-out) order.

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2") // Prints 2, then 1
}

14. Panic in Deferred Functions

A panic in a deferred function can overwrite an earlier panic, complicating debugging.

func main() {
    defer func() {
        panic("deferred panic") // Overwrites original panic
    }()
    panic("original panic")
}

Use recover carefully to handle panics.

15. Incorrect recover Usage

recover only works in deferred functions and captures panics in the current goroutine.

// Wrong
func main() {
    recover() // Does nothing
    panic("test")
}

// Correct
func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    panic("test")
}

16. Goroutines Without Synchronization

Goroutines run concurrently, and unsynchronized access to shared variables causes race conditions.

// Wrong
var x int
go func() { x++ }()
go func() { x++ }()

Use sync.Mutex or channels for synchronization.

var (
    x    int
    mu   sync.Mutex
)
mu.Lock()
x++
mu.Unlock()

17. Goroutines Leaking

Goroutines that never exit (e.g., blocked on channels) can leak memory.

// Wrong
func main() {
    ch := make(chan int)
    go func() {
        <-ch // Blocks forever
    }()
}

Use context cancellation or close channels to signal exit.

18. Closing Unbuffered Channels

Closing an unbuffered channel before a receiver is ready causes a panic.

// Wrong
ch := make(chan int)
close(ch)
<-ch // Panic: receive from closed channel

Ensure receivers are ready or use buffered channels.

19. Sending to a Closed Channel

Sending to a closed channel causes a panic.

ch := make(chan int, 1)
close(ch)
ch <- 1 // Panic: send on closed channel

Check if a channel is closed using a select or boolean flag.

20. Misusing Buffered Channels

Buffered channels can block if full, leading to deadlocks if not handled.

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

Monitor buffer size or use non-blocking sends with select.

21. Incorrect Range Over Channels

Ranging over a channel continues until the channel is closed.

ch := make(chan int)
go func() {
    ch <- 1
    // Channel not closed
}()
for v := range ch {
    fmt.Println(v) // Blocks indefinitely
}

Always close channels when done sending.

22. Nil Channels

Sending or receiving on a nil channel blocks forever.

var ch chan int // nil
ch <- 1 // Blocks

Initialize channels with make before use.

23. Incorrect Interface Assertions

Type assertions on interfaces can panic if the type doesn’t match.

var i interface{} = "hello"
x := i.(int) // Panic: interface conversion

Use the comma-ok idiom to check safely.

x, ok := i.(int)
if !ok {
    fmt.Println("Not an int")
}

24. Empty Interface Misuse

Using interface{} sacrifices type safety and can lead to runtime errors.

func process(i interface{}) {
    x := i.(int) // Risky
}

Prefer specific interfaces or generics (Go 1.18+).

25. String Conversion Errors

Converting non-numeric strings to integers with strconv.Atoi returns an error that must be checked.

// Wrong
x, _ := strconv.Atoi("abc") // Ignores error

Always handle errors.

x, err := strconv.Atoi("abc")
if err != nil {
    log.Fatal(err)
}

26. Incorrect String Concatenation

Repeated string concatenation in a loop is inefficient due to immutability.

// Wrong
s := ""
for _, v := range []string{"a", "b", "c"} {
    s += v // Creates new strings
}

Use strings.Builder for efficiency.

var b strings.Builder
for _, v := range []string{"a", "b", "c"} {
    b.WriteString(v)
}
s := b.String()

27. Map Initialization

Using an uninitialized map (var m map[K]V) causes a panic on write.

var m map[int]int
m[1] = 1 // Panic: assignment to entry in nil map

Initialize with make or a literal.

m := make(map[int]int)

28. Map Iteration Order

Map iteration order is random by design, which can surprise developers expecting consistency.

m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
    fmt.Println(k, v) // Order varies
}

Sort keys explicitly if order matters.

29. Concurrent Map Access

Maps are not safe for concurrent writes.

// Wrong
m := make(map[int]int)
go func() { m[1] = 1 }()
go func() { m[1] = 2 }() // Race condition

Use sync.RWMutex or sync.Map for concurrency.

30. Incorrect Map Deletion

Deleting from a map with a non-existent key is safe but has no effect.

m := map[int]int{1: 100}
delete(m, 2) // No-op

Check existence with the comma-ok idiom if needed.

_, ok := m[2]

31. Incorrect Struct Field Access

Accessing unexported fields (lowercase) from another package causes compilation errors.

// package a
type S struct {
    x int // unexported
}

// package b
s := a.S{}
s.x = 10 // Error: cannot refer to unexported field

Use exported fields (uppercase).

32. Struct Copy Semantics

Structs are copied by value, which can lead to unexpected behavior.

type S struct { x int }
s1 := S{x: 1}
s2 := s1
s2.x = 2
fmt.Println(s1.x) // Prints 1

Use pointers for shared modifications.

33. JSON Marshalling Unexported Fields

Unexported struct fields are ignored during JSON marshalling.

type S struct {
    x int // Ignored
    Y int
}
s := S{x: 1, Y: 2}
b, _ := json.Marshal(s) // {"Y":2}

Use exported fields for JSON.

34. Incorrect Time Comparison

Comparing time.Time values with == checks both time and location.

t1 := time.Now()
t2 := t1.UTC()
fmt.Println(t1 == t2) // False

Use Equal for time comparison.

fmt.Println(t1.Equal(t2)) // True

35. Time Parsing Errors

Parsing invalid time strings with time.Parse returns an error that must be checked.

// Wrong
t, _ := time.Parse(time.RFC3339, "invalid")

Always handle errors.

36. Incorrect Error Wrapping

Using fmt.Errorf without wrapping the original error loses context.

// Wrong
return fmt.Errorf("failed: %s", err)

Use %w to wrap errors (Go 1.13+).

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

37. Ignoring Context Cancellation

Ignoring context.Context cancellation can lead to resource leaks.

// Wrong
func process(ctx context.Context) {
    time.Sleep(time.Hour) // Ignores cancellation
}

Check ctx.Done().

select {
case <-ctx.Done():
    return ctx.Err()
default:
    // Proceed
}

38. Incorrect HTTP Handler Registration

Registering HTTP handlers with the same path overwrites earlier handlers.

http.HandleFunc("/path", handler1)
http.HandleFunc("/path", handler2) // Overwrites handler1

Use a router like http.ServeMux carefully.

39. HTTP Server Shutdown

Not shutting down HTTP servers gracefully can drop connections.

// Wrong
http.ListenAndServe(":8080", nil)

Use http.Server.Shutdown.

srv := &http.Server{Addr: ":8080"}
go srv.ListenAndServe()
srv.Shutdown(ctx)

40. Incorrect File Closing

Not closing files can leak resources.

// Wrong
f, _ := os.Open("file.txt")

Use defer to close.

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

41. Incorrect Loop Variable Capture

Loop variables in closures (e.g., goroutines) are reused, causing unexpected behavior.

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

Copy the loop variable.

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

42. Incorrect Array vs. Slice

Arrays ([n]T) and slices ([]T) have different behaviors. Arrays are fixed-size and copied by value.

arr := [3]int{1, 2, 3}
s := arr[:]
s[0] = 99
fmt.Println(arr) // [99, 2, 3]

43. Incorrect Pointer Receiver

Using value receivers in methods prevents modifying the receiver.

type S struct { x int }
func (s S) Set(x int) { s.x = x } // No effect

Use pointer receivers.

func (s *S) Set(x int) { s.x = x }

44. Nil Pointer Dereference

Dereferencing a nil pointer causes a panic.

var p *int
*p = 1 // Panic: nil pointer dereference

Check for nil before dereferencing.

45. Incorrect Method Expression

Using method expressions requires explicit receiver passing.

type S struct{}
func (s S) M() { fmt.Println("M") }

// Wrong
f := S.M // Error

// Correct
f := (*S).M
var s S
f(&s)

46. Incorrect Interface Implementation

A type must implement all methods of an interface, including pointer vs. value receiver nuances.

type I interface { M() }
type T struct{}
func (t T) M() {}

// Wrong
var i I = T{} // OK
var i2 I = &T{} // Error if I requires pointer

47. Incorrect Slice Initialization

Slices must be initialized before use, but make syntax varies.

// Wrong
s := make([]int) // Error: make requires len

Use correct make arguments.

s := make([]int, 0)

48. Incorrect Range Over Strings

Ranging over a string iterates over bytes, not runes, which can break UTF-8 handling.

s := "hello世界"
for i, c := range s {
    fmt.Printf("%d: %c\n", i, c) // May split runes
}

Use []rune for rune iteration.

for i, c := range []rune(s) {
    fmt.Printf("%d: %c\n", i, c)
}

49. Incorrect Signal Handling

Not handling OS signals can prevent graceful shutdown.

// Wrong
http.ListenAndServe(":8080", nil)

Use os/signal.

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)
<-sig

50. Ignoring Go’s Simplicity

Overcomplicating solutions with unnecessary abstractions violates Go’s philosophy of simplicity.

// Overcomplicated
type Processor interface {
    Process(context.Context, interface{}) interface{}
}

// Simple
func process(ctx context.Context, data int) (int, error)

Embrace straightforward, idiomatic Go.

Conclusion

These 50 mistakes cover syntax, concurrency, memory management, and idiomatic practices critical for Go development. By understanding these pitfalls and their solutions, you can write cleaner, more efficient, and maintainable Go code. Refer to the Go documentation and tools like go vet, golint, and golangci-lint to catch these issues early.

Last updated