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
nil
SlicesA 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
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
defer
OrderDeferred 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
Usagerecover
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