Avoid These Common Go Pitfalls Before They Crash Your Code
This article compiles a series of frequent Go programming pitfalls—ranging from incorrect use of unsafe.Sizeof and variadic any parameters to slice expansion, pointer handling, closure capture, concurrency bugs, and serialization quirks—providing concrete code examples and safe alternatives to help developers write more reliable Go code.
1. Parameter Passing Misuse
Using unsafe.Sizeof on a pointer always returns the size of the pointer (8 bytes on 64‑bit platforms), which can lead to incorrect size calculations.
func TestSizeofPtrBug(t *testing.T) {
type CodeLocation struct {
LineNo int64
ColNo int64
}
cl := &CodeLocation{10, 20}
size := unsafe.Sizeof(cl)
fmt.Println(size) // always returns 8
}Solution: Write a helper that returns the size of the underlying value.
func TestSizeofPtrWithoutBug(t *testing.T) {
type CodeLocation struct { LineNo int64; ColNo int64 }
cl := &CodeLocation{10, 20}
size := ValueSizeof(cl)
fmt.Println(size) // 16
}
func ValueSizeof(v any) uintptr {
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Pointer {
return typ.Elem().Size()
}
return typ.Size()
}1.2 Variadic any Parameters
When a variadic parameter has type any, passing a slice without the ellipsis treats the whole slice as a single element.
appendAnyF := func(t []any, toAppend ...any) []any { return append(t, toAppend...) }
emptySlice := []any{}
slice2 := []any{"hello", "world"}
// Bug: appending slice as one element
emptySlice = appendAnyF(emptySlice, slice2)
fmt.Println(emptySlice) // [[hello world]]
// Correct usage with ellipsis
emptySlice = []any{}
emptySlice = appendAnyF(emptySlice, slice2...)
fmt.Println(emptySlice) // [hello world]1.3 Array Value Semantics
Arrays are passed by value; modifying a parameter does not affect the original array.
arr := [3]int{0, 1, 2}
f := func(v [3]int) { v[0] = 100 }
f(arr)
fmt.Println(arr) // [0 1 2]1.4 Slice Expansion Breaks Sharing
Appending to a slice may allocate new backing storage, breaking the link with the original array.
arr := []int{0, 1, 2}
f := func(v []int) {
v[0] = 100 // modifies original
v = append(v, 4) // new allocation
v[0] = 50 // does NOT modify original
}
f(arr)
fmt.Println(arr) // [100 1 2]1.5 Returning Shared Slice References
Returning a slice that shares underlying memory can unintentionally mutate the source data; copy the slice before returning.
type Queue struct { content []byte; pos int }
func (q *Queue) ReadUnsafe(size int) []byte {
if q.pos+size >= len(q.content) { return nil }
pos := q.pos
q.pos += size
return q.content[pos:q.pos]
}
func (q *Queue) ReadSafe(size int) []byte {
if q.pos+size >= len(q.content) { return nil }
pos := q.pos
q.pos += size
ret := make([]byte, size)
copy(ret, q.content[pos:q.pos])
return ret
}2. Pointer‑Related Pitfalls
2.1 Storing uintptr Values
Saving a pointer as uintptr loses the connection to the actual memory address; the compiler does not track changes.
slice := []int{0, 1, 2}
ptr := unsafe.Pointer(&slice[0])
slice = append(slice, 3) // new allocation
ptr2 := unsafe.Pointer(&slice[0])
fmt.Printf("ptr is %d, ptr2 is %d, equal? %v
", ptr, ptr2, ptr == ptr2)2.2 len/cap on nil vs empty slices/maps
Both nil and empty slices (or maps) return length and capacity zero.
var s []int = nil
fmt.Println(len(s), cap(s)) // 0 0
s2 := []int{}
fmt.Println(len(s2), cap(s2)) // 0 0
var m map[int]int = nil
fmt.Println(len(m)) // 0
m2 := map[int]int{}
fmt.Println(len(m2)) // 02.3 Using new for maps
new(map[int]int)creates a nil map; attempts to assign to it panic. Use make instead.
mp := new(map[int]int)
func(m map[int]int) { m[10] = 10 }
// panic: assignment to entry in nil map2.4 Nil Interface vs Nil Pointer Interface
A nil concrete pointer assigned to an interface value is not a nil interface; comparisons to nil are false.
type MyErr struct{}
func (e *MyErr) Error() string { return "" }
var e *MyErr = nil
var e2 error = e // e2 != nil
fmt.Println(e2 == nil) // false3. Function, Method and Control‑Flow Issues
3.1 Closure Capturing Loop Variable
Closures capture the loop variable itself; after the loop ends the variable holds the final value, causing out‑of‑bounds errors.
type S struct { A, B, C string }
typ := reflect.TypeOf(S{})
funcArr := make([]func() string, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
f := func() string { return typ.Field(i).Name }
funcArr[i] = f
}
fmt.Println(funcArr[0]()) // panic: index out of boundsFix: copy the index into a new variable inside the loop.
for i := 0; i < typ.NumField(); i++ {
idx := i
f := func() string { return typ.Field(idx).Name }
funcArr[i] = f
}
fmt.Println(funcArr[0]()) // A3.2 Range on Large Elements
Iterating with range copies each element; for large structs this can be much slower than a manual index loop.
func CreateABigSlice(count int) [][4096]int {
ret := make([][4096]int, count)
for i := 0; i < count; i++ { ret[i] = [4096]int{} }
return ret
}
func BenchmarkRangeHiPerformance(b *testing.B) {
v := CreateABigSlice(1 << 12)
for i := 0; i < b.N; i++ {
var tmp [4096]int
for k := 0; k < len(v); k++ { tmp = v[k] }
_ = tmp
}
}
func BenchmarkRangeLowPerformance(b *testing.B) {
v := CreateABigSlice(1 << 12)
for i := 0; i < b.N; i++ {
var tmp [4096]int
for _, e := range v { tmp = e }
_ = tmp
}
}
// Result: range version ~10,000× slower.3.3 Defer Inside Loops
Deferring a resource release inside a loop delays the close until the surrounding function returns, potentially exhausting resources.
for i := 0; i < 5; i++ {
f, err := os.Open("./mygo.go")
if err != nil { log.Fatal(err) }
defer f.Close() // delayed
}Fix: close explicitly inside the loop.
for i := 0; i < 5; i++ {
f, err := os.Open("/path/to/file")
if err != nil { log.Fatal(err) }
f.Close()
}3.4 Goroutine May Exit Prematurely
A goroutine that panics without recovery terminates the whole program.
go func() { panic("oh...") }()
for i := 0; i < 3; i++ { fmt.Println(i); time.Sleep(time.Second) }
fmt.Println("bye bye!") // never reachedRecover inside the goroutine:
go func() {
defer func() { recover() }()
panic("oh...")
}()4. Concurrency and Memory Synchronization
Go's memory model guarantees write ordering only within a single goroutine; without explicit synchronization, other goroutines may observe stale values.
var msg string
var done bool
func setup() { msg = "hello, world"; done = true }
func main() {
go setup()
for !done {}
println(msg) // may print empty string
}Use a channel to synchronize:
var msg string
var done = make(chan bool)
func setup() { msg = "hello, world"; done <- true }
func main() {
go setup()
<-done
println(msg) // guaranteed "hello, world"
}5. Serialization Gotchas
Unmarshalling into a map does not delete existing keys; subsequent unmarshals only add or overwrite keys, leaving stale entries.
val := map[string]int{}
s1 := `{"k1":1,"k2":2,"k3":3}`
s2 := `{"k1":11,"k2":22,"k4":44}`
json.Unmarshal([]byte(s1), &val) // {k1:1 k2:2 k3:3}
json.Unmarshal([]byte(s2), &val) // {k1:11 k2:22 k3:3 k4:44}Solution: re‑declare the variable before each unmarshal if you need a clean map.
6. Miscellaneous
6.1 Numeric Overflow When Shifting
Shifting a small‑type value before converting can overflow.
var num int16 = 5000
var result int64 = int64(num << 9) // overflow, prints 4096
// Fixed:
var result int64 = int64(num) << 9 // correct 25600006.2 Map Iteration Order Is Random
Go maps iterate in a nondeterministic order; do not rely on ordering.
mp := map[int]int{}
for i := 0; i < 20; i++ { mp[i] = i }
for k, v := range mp { fmt.Println(k, v) } // order varies each runSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
