Go has no null. Every type has a well-defined zero value instead.
The zero value table
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string, not nil)
var p *int // nil
var sl []int // nil (a nil slice, but len(sl) == 0 works fine)
var m map[string]int // nil (readable, but writing to it panics)
var ch chan int // nil
Why this matters in practice
A struct with no explicit initializer is already fully usable:
type Config struct {
Retries int
Timeout float64
Name string
}
var c Config
fmt.Println(c.Retries, c.Timeout, c.Name) // 0 0 ""
No field is ever an uninitialized garbage value or a null pointer waiting to crash your program. This is one reason Go code has fewer null-check branches than languages where uninitialized means undefined or null.
The one gotcha: nil maps
A nil map behaves like an empty map for reads, but panics on write:
var m map[string]int
fmt.Println(m["x"]) // 0, no panic — reading a missing key is safe
m["x"] = 1 // panic: assignment to entry in nil map
Always initialize a map you intend to write to, with make(map[string]int) or a map literal.
Related Resources
Arrays are rare in everyday Go code. Slices are what you actually use.
The slice header
A slice value is really this, conceptually:
type sliceHeader struct {
ptr *T
len int
cap int
}
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // len=2, cap=4 (from index 1 to end of arr)
fmt.Println(s) // [2 3]
s[0] = 99
fmt.Println(arr) // [1 99 3 4 5] — s shares arr's backing array
What happens on append
s := make([]int, 2, 2) // len=2, cap=2
s = append(s, 3) // cap exceeded: new backing array allocated, data copied
If len(s) < cap(s), append writes into the existing backing array. If not, Go allocates a new array (usually doubling capacity for smaller slices) and copies everything over. This is why appending to a slice sometimes affects a second slice sharing its backing array, and sometimes doesn't — it depends on whether that specific append triggered reallocation.
The classic slicing bug
func trimFirst(s []int) []int {
return s[1:] // still shares the same backing array as the caller's slice
}
Sub-slicing never copies. If you need an independent copy, use copy() explicitly:
dst := make([]int, len(src))
copy(dst, src)
Related Resources
Maps look simple, but four specific behaviors trip up almost everyone new to Go.
Checking for existence vs. zero value
m := map[string]int{"a": 1}
v := m["b"] // 0 — can't tell "missing" from "present with value 0"
v, ok := m["b"] // v=0, ok=false — this is the idiomatic check
if ok {
fmt.Println("found:", v)
}
Randomized iteration order
for k, v := range m {
fmt.Println(k, v) // order changes between runs, by design
}
Go randomizes map iteration order deliberately, to stop anyone depending on it. If you need sorted output, collect the keys and sort them yourself:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
Concurrent access panics
// Running this with -race, or under real concurrent load, panics:
// "fatal error: concurrent map writes"
go func() { m["x"] = 1 }()
go func() { m["y"] = 2 }()
Maps have zero built-in concurrency safety. Guard shared maps with a sync.Mutex, or use sync.Map for specific high-read, low-write patterns.
No addressable map values
type Point struct{ X, Y int }
m := map[string]Point{"origin": {0, 0}}
m["origin"].X = 5 // compile error: cannot assign to struct field
Map values aren't addressable, since the runtime may relocate them during a rehash. Fix it by reassigning the whole value, or storing pointers (map[string]*Point) instead.
Related Resources
Getting this distinction wrong is the single most common source of "why didn't my change show up" bugs in Go.
Value types copy on assignment
type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := p1 // full copy
p2.X = 99
fmt.Println(p1.X) // still 1 — p1 was never touched
func modify(arr [3]int) {
arr[0] = 99 // modifies the local copy only
}
a := [3]int{1, 2, 3}
modify(a)
fmt.Println(a) // [1 2 3] — unchanged
Reference-like types share underlying data
func modify(s []int) {
s[0] = 99 // writes through to the shared backing array
}
sl := []int{1, 2, 3}
modify(sl)
fmt.Println(sl) // [99 2 3] — changed
Table of behavior
| Type | Copy behavior on assignment/pass |
|---|---|
struct, array | Full copy of all fields/elements |
| slice, map, channel | Header/reference copied; underlying data shared |
| pointer | Address copied; both variables point to the same value |
| function value | Reference to the function; safe to copy and pass around |
Opting a struct into reference-like behavior
func modify(p *Point) {
p.X = 99 // dereferences and writes through the pointer
}
pt := Point{1, 2}
modify(&pt)
fmt.Println(pt.X) // 99
This is exactly why a method meant to mutate its receiver needs a pointer receiver — a value receiver method only ever sees a copy.
Related Resources
defer is Go's answer to finally blocks, but its exact evaluation timing surprises newcomers.
Basic usage
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns, no matter which path
// ... use f ...
return nil
}
LIFO ordering
func demo() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// Output: 3
// 2
// 1
Pitfall: arguments evaluated immediately
func demo() {
x := 1
defer fmt.Println("x was:", x) // "x" captured as 1 right now
x = 2
}
// Output: "x was: 1", not 2
If you need the final value, defer a closure instead:
defer func() { fmt.Println("x is now:", x) }() // reads x at call time
Pitfall: defer in a loop
func processFiles(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // BUG: all files stay open until processFiles returns
}
return nil
}
With hundreds of files, this can exhaust the OS's file descriptor limit before processFiles ever returns. Fix it by wrapping the loop body in its own function, so each defer fires per iteration:
func processOne(p string) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()
// ... use f ...
return nil
}