Zero Values

How Go assigns safe defaults to every variable, why that matters, and how to avoid the most common zero-value traps.

Every variable in Go has a value, even before you assign one. The language guarantees that a declaration like var x int does not leave x in an unpredictable state — it sets x to the zero value for its type. This is not an accident or a convenience feature; it is a deliberate part of the language’s memory model, safety guarantees, and design philosophy.

What Zero Values Are

A zero value is the default content a variable holds when it is declared but not explicitly initialized. The value depends only on the type, never on where the variable lives (stack or heap) or how it was declared. Whether you use var, new, or a composite literal with no fields specified, the result is the same: every bit of the variable’s memory is set to zero, and the language interprets that zero pattern as the natural “empty” state for the type.

This is different from C or C++, where an uninitialized variable holds whatever bits happened to be in memory. It is also different from languages that force you to initialize everything before use. Go takes a middle path: every variable is immediately safe to read, and for many types the zero value is immediately useful.

Zero means zero in memory:

Go actually writes zeros to the memory occupied by the variable. A bool is false because false is represented as 0. A pointer is nil because nil is the zero bit pattern for pointers. This is not a coincidence — it makes the runtime and garbage collector simpler and faster.

Zero Values for Basic Types

Every basic type has a single, fixed zero value:

TypeZero value
boolfalse
int, int8, int64, and all other signed/unsigned integer types0
float32, float640.0
complex64, complex1280+0i
string"" (the empty string)
byte (alias for uint8)0
rune (alias for int32)0

You can verify all of these in a few lines:

package main
import "fmt"
func main() {
    var (
        b bool
        i int
        f float64
        c complex128
        s string
        by byte
        r rune
    )
    fmt.Printf("bool: %v\n", b)
    fmt.Printf("int: %v\n", i)
    fmt.Printf("float64: %v\n", f)
    fmt.Printf("complex128: %v\n", c)
    fmt.Printf("string: %q\n", s)
    fmt.Printf("byte: %v\n", by)
    fmt.Printf("rune: %v (char: %q)\n", r, r)
}

The output is predictable every time: false, 0, 0, (0+0i), "", 0, and 0 (with the rune printing as the null character '\x00'). There is no undefined behavior, no uninitialized memory leak, and no need to check whether a basic-typed variable “has a value.”

This property is especially useful when a zero value naturally works as a sensible starting point — a bool starts as false (a safe default for flags), an int starts at 0 (a natural counter), and a string starts empty (ready to be built up with concatenation or strings.Builder).

Zero Values for Reference Types

Types that hold references to underlying data — pointers, slices, maps, channels, functions, and interfaces — all have a zero value of nil. That single word covers several different behaviors, and not all of them are safe to use without initialization.

TypeZero value
Pointer (*T)nil
Slice ([]T)nil
Map (map[K]V)nil
Channel (chan T)nil
Function (func(...) ...)nil
Interface (interface{ ... })nil

Declare any of these without an initializer and you get nil:

var p *int
var s []int
var m map[string]int
var ch chan int
var fn func()
var iface interface{}

A nil pointer cannot be dereferenced — doing so panics. A nil map can be read safely (reads return the zero value of the value type), but writing to it panics. A nil channel blocks forever when you try to send or receive on it. A nil function panics when called. The only exception is a nil slice: you can call len, cap, append, and range on it without any problem.

Zero Values for Structs and Arrays

An array or struct is not a reference; it holds its data directly. So it is never nil. Instead, every element of the array and every field of the struct is recursively set to its own type’s zero value.

type Address struct {
    City   string
    Zip    int
}
type Person struct {
    Name    string
    Age     int
    Active  bool
    Home    Address
}
var p Person
fmt.Printf("%+v\n", p)

The output shows that every field, nested or not, has its zero value:

{Name: Age:0 Active:false Home:{City: Zip:0}}

Arrays work the same way. var arr [4]int is [0, 0, 0, 0]. No element is left uninitialized.

This property is the foundation of one of Go’s most important design patterns: types whose zero value is ready to use without a constructor.

The “Zero-Value-Is-Useful” Design Philosophy

Go’s standard library and idiomatic community code are built around the idea that a type’s zero value should be meaningful. A sync.Mutex is usable as soon as you declare it — you don’t need to call NewMutex. A bytes.Buffer is ready to absorb writes at its zero value. An http.Server with just the Addr field set works for serving requests.

This philosophy reduces boilerplate. Instead of forcing every type to export a constructor and requiring callers to check for allocation errors, Go lets you write:

var mu sync.Mutex
mu.Lock()
// ... critical section ...
mu.Unlock()

A struct that relies on slices benefits even more. Consider a simple stack:

type Stack struct {
    items []string
}
func (s *Stack) Push(v string) {
    s.items = append(s.items, v)
}
func (s *Stack) IsEmpty() bool {
    return len(s.items) == 0
}

A var s Stack starts with items as nil. But because append works on nil slices, the stack is functional immediately — no constructor needed.

No constructor required:

When you design your own types, aim for this same property. If a var t T (or T{}) gives the user a working value, you eliminate an entire category of “did I forget to call New?” bugs. The user can start using the value directly.

Common Pitfalls and How to Avoid Them

The predictability of zero values is not a guarantee of safety for every operation. Several behaviors catch both newcomers and experienced developers.

Writing to a nil map

var m map[string]int
m["key"] = 1   // panic: assignment to entry in nil map

Reading from a nil map is fine — m["key"] returns 0, the zero value of int. Writing to it is not. Fix by initializing the map before storing entries:

m = make(map[string]int)
m["key"] = 1

Nil map reads are safe, writes panic:

This asymmetry is a frequent source of panics in code that branches conditionally. If a map might be nil, always check or initialize it before writing.

Sending to or receiving from a nil channel

var ch chan int
ch <- 1      // blocks forever
<-ch         // blocks forever

A nil channel blocks indefinitely. This is occasionally useful — for example, to disable a select case — but more often it is a bug that causes a goroutine leak. Always use make(chan int) before communicating.

A nil channel blocks forever without a panic:

Unlike a nil map write, a nil channel operation does not crash your program. It silently deadlocks the goroutine, which is harder to detect. Run your code with the race detector and monitor goroutine counts during testing.

Typed nil inside an interface

An interface value has two parts: a type pointer and a value pointer. An interface is nil only when both are nil. If you assign a typed nil (like a *os.PathError that is nil) to an interface variable, the interface is not nil because its type pointer is set.

var err error
fmt.Println(err == nil) // true
err = (*os.PathError)(nil)
fmt.Println(err == nil) // false

This is the root cause of many “error is not nil but the value is nil” bugs. When returning errors from functions, always return a plain nil, not a typed nil.

Calling a nil function

var fn func()
fn() // panic: runtime error: invalid memory address or nil pointer dereference

A function variable is a pointer to executable code. A nil function pointer cannot be called. If you store functions in maps or struct fields, check for nil before invoking.

Zero Values and the new Function

The built-in new(T) allocates memory for a value of type T, sets it to the zero value of T, and returns a pointer to it. It works for any type, though it is most commonly seen with structs.

p := new(Person)
// p is *Person, and *p is {Name: "", Age: 0, Active: false, Home: {City: "", Zip: 0}}

For basic types, new(int) gives you a pointer to 0. While you can write i := new(int), the same effect is often achieved with a composite literal like i := new(int) (same) or simply declaring a variable and taking its address. new is less common for basic types but fully valid.

Nil Slices vs. Empty Slices

A nil slice and an empty slice are not the same, even though both have length 0 and print as []. The difference matters for JSON encoding and for performance.

var nilSlice []int
emptySlice := []int{}
fmt.Println(nilSlice == nil)  // true
fmt.Println(emptySlice == nil) // false

When marshaled to JSON, a nil slice becomes null, while an empty slice becomes []. If your API contract expects [] even when no elements exist, use an empty slice. If null is acceptable, a nil slice is fine and avoids allocating a backing array.

b1, _ := json.Marshal(nilSlice)   // "null"
b2, _ := json.Marshal(emptySlice) // "[]"

In terms of performance, a nil slice doesn’t allocate memory for a backing array. An empty slice created with []int{} does allocate a small header. The difference is negligible in most code, but in hot paths, preferring nil slices can reduce allocations.

Why Go Guarantees Zero Values

The Go runtime must initialize all memory to a known state because it is a garbage-collected language. If a pointer held random bits, the garbage collector could interpret those bits as valid object references, causing memory corruption or preventing collection of live data. Zeroing all memory ensures that every pointer starts as nil, making the heap safe to scan from the moment a variable exists.

Beyond that low-level requirement, the language’s designers made a higher-level choice: embrace zero values as a feature, not just a safety net. By making 0, "", false, and nil valid for all types, Go eliminates whole classes of uninitialized-variable bugs. The developer does not have to remember to set a default; the language does it automatically and deterministically.

Other modern languages solve this differently. Rust prevents reading uninitialized memory at compile time. Java and C# default to null or zero, but treat structs as heap-allocated objects with constructor requirements. Go splits the difference: value types hold data directly and are zero-initialized in place; reference types hold nil and require explicit initialization before dangerous operations. The result is a language that feels safe without forcing you to write constructors for every type.

Summary

A zero value in Go is a guaranteed, type-specific default that every variable receives at declaration. For basic types it is 0, false, "", or the equivalent. For references it is nil. For compound types, the zeroing recurses through every field and element. This design eliminates uninitialized memory bugs and allows many types to be usable without constructors, which is why the standard library is full of values that work right out of the box.

The three most important takeaways for daily Go code are:

  • A nil slice is safe for append, len, and range; nil maps are safe for reads but not writes; nil channels block forever.
  • An interface holding a typed nil is not itself nil — always return plain nil for errors.
  • When you design a struct, ask whether var t T gives a working value. If not, you are pushing initialization cost onto every caller.