Fun and Surprising Go Facts You Probably Didn’t Know!

Here are some lesser-known Go (Golang) facts that even go developers might not know:
1. Go’s nil
Has Its Own Quirks
In Go, the nil
value behaves unexpectedly when applied to interfaces. An interface can be nil
, but its underlying type and value also determine how nil
behaves.
Example:
1
2
3
4
5
6
7
var i interface{} = (*int)(nil)
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is NOT nil")
}
Output:
1
i is NOT nil
Why? Because while the underlying value is nil
, the interface itself still holds a type (*int
) and isn’t considered completely nil.
2. Go’s defer
Uses LIFO (Last-In, First-Out)
When multiple defer
statements are used, they are executed in reverse order — like a stack.
Example:
1
2
3
4
5
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
Output:
1
2
3
3
2
1
This LIFO execution order is intentional and can be used strategically, such as ensuring resources are released in the opposite order they were acquired.
3. Go Has a Built-in Memory Alignment Trick
Go automatically aligns fields in a struct to optimize memory usage, but the order of fields matters.
Example:
1
2
3
4
5
type Example struct {
A byte
B int64
C byte
}
Here, Go will add padding between fields to maintain memory alignment, resulting in more memory consumption than expected. Rearranging the fields can fix this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type ExampleOptimized struct {
B int64
A byte
C byte
}
func main() {
fmt.Printf("Size of Misaligned struct: %d bytes\n", unsafe.Sizeof(Misaligned{}))
fmt.Printf("Size of Aligned struct: %d bytes\n", unsafe.Sizeof(Aligned{}))
fmt.Printf("\nField offsets in Misaligned struct:\n")
fmt.Printf("A: %d\n", unsafe.Offsetof(Misaligned{}.A))
fmt.Printf("B: %d\n", unsafe.Offsetof(Misaligned{}.B))
fmt.Printf("C: %d\n", unsafe.Offsetof(Misaligned{}.C))
fmt.Printf("\nField offsets in Aligned struct:\n")
fmt.Printf("B: %d\n", unsafe.Offsetof(Aligned{}.B))
fmt.Printf("A: %d\n", unsafe.Offsetof(Aligned{}.A))
fmt.Printf("C: %d\n", unsafe.Offsetof(Aligned{}.C))
}
Output:
1
2
3
4
5
6
7
8
9
10
11
12
Size of Example struct: 24 bytes
Size of ExampleOptimized struct: 16 bytes
Field offsets in Example struct:
A: 0
B: 8
C: 16
Field offsets in ExampleOptimized struct:
B: 0
A: 8
C: 9
By grouping fields of similar size together, you can reduce memory usage significantly.
4. Go’s Empty Slice Isn’t Always nil
An uninitialized slice is nil
, but a slice created with make()
or an empty literal is not nil
, even if it has no elements.
Example:
1
2
3
4
5
6
7
var s1 []int
s2 := make([]int, 0)
s3 := []int{}
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(s3 == nil) // false
Understanding this distinction is critical when writing code that checks for “empty slices.”
5. Go’s Complex select
Behavior in Deadlocks
Go’s select
statement, while powerful, has edge cases that can lead to unexpected behavior, especially with deadlocks. If all channels in a select
block are blocked and there is no default
case, the program deadlocks, but Go’s runtime handles this differently depending on the context.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(1 * time.Second)
ch1 <- 42
}()
select {
case val := <-ch1:
fmt.Println("Received from ch1:", val)
case val := <-ch2:
fmt.Println("Received from ch2:", val)
}
}
Behavior: If neither channel is ready and there’s no default
case, the select
blocks indefinitely. However, Go’s runtime avoids spinning CPU cycles inefficiently and places the goroutine into a sleep state until one of the channels becomes available. This behavior is subtle and can lead to tricky bugs when reasoning about concurrency.