Go's Value Philosophy: Part 3 - Zero Values: Go's Valid-by-Default Philosophy
Deep dive into Go's zero values: how declaration creates valid values, why Go has no uninitialized state, and how this eliminates entire classes of bugs that plague null-based languages.
- categories
- Programming Go
- published
📚 Series: Go Value Philosophy
- Go's Value Philosophy: Part 1 - Why Everything Is a Value, Not an Object
- Go's Value Philosophy: Part 2 - Escape Analysis and Performance
- Go's Value Philosophy: Part 3 - Zero Values: Go's Valid-by-Default Philosophy (current)
In Part 1 , we explored Go’s value philosophy and how it differs from Python’s objects and Java’s classes. Part 2 revealed how escape analysis makes value semantics performant. Now we address a fundamental question about value semantics:
What happens when you declare a variable?
| |
In Python, undeclared variables don’t exist (NameError).
In Java, local variables must be assigned before use (compile error).
In Go, x exists immediately as the value 0 - the zero value for integers.
Declaration vs initialization:
- Declaration: Announcing a variable exists and reserving memory for it
- Initialization: Giving that variable its first value
Reserve memory] --> i1[Uninitialized state] --> a1[Assignment
First value] end subgraph go["Go"] d2[Declaration
Reserve memory] --> i2[Zero value
Immediate value] end style other fill:#4C4538,stroke:#6b7280,color:#f0f0f0 style go fill:#3A4C43,stroke:#6b7280,color:#f0f0f0
In most languages, these are separate steps. You declare a variable (reserve space), then initialize it (give it a value).
Go merges these: declaration IS initialization. When you declare var x int, you don’t get uninitialized memory - you get the integer 0.
Every Go variable is initialized at the moment it’s declared.
Zero Values: Go’s Valid-by-Default Philosophy
In Go, declaration creates a value. When you write var x int, you don’t get an uninitialized variable - you get the integer 0. This is the zero value for integers.
Every type in Go has a zero value - the state a variable holds from the moment it’s declared. No null, no undefined, no uninitialized memory. Declaration equals instantiation.
This simple design choice removes entire classes of null-related runtime failures and enables API designs impossible in languages where variables can be uninitialized or null.
Declaration Creates Values
The fundamental difference between Go and other languages is what happens when you declare a variable:
Go: Declaration creates a valid value
| |
Python: Assignment creates variables
| |
Java: Local variables forbidden before assignment
| |
var is itself an initialization. Local variables must still be definitely assigned along all control paths - but the var form guarantees that assignment happens at declaration. The zero value IS the value, not a placeholder for a future value.The Nil Paradox: How “Valid” Still Includes nil
Wait - if Go is “valid by default,” why does nil exist?
Go’s zero value philosophy has a nuance: some types have nil as their zero value (pointers, slices, maps, channels, interfaces, functions). This seems contradictory - how can “every value is valid by default” coexist with nil? The answer reveals a fundamental design choice about what “valid” means.
In Java and Python, null/None represents the absence of an object. Any operation on null crashes. The value is invalid - it can’t be used until you explicitly check for null and handle that case.
Go’s nil is different. It represents a valid zero state that supports specific operations. The type determines which operations work. For some types (slices), nil supports nearly all read operations. For others (maps), nil supports reads but not writes. For pointers, nil can be checked but not dereferenced.
The pattern: Go’s nil values have well-defined, predictable behavior rather than universal failure.
Go’s nil slice - safe for reading:
| |
A nil slice behaves like an empty slice for read operations. You can check its length, iterate over it (which completes immediately), and append to it (which allocates storage on first append). The nil state is the zero state - it’s not an error condition requiring defensive checks everywhere.
Nil slices and empty slices often behave the same for reads, but they’re not identical: nil slices compare equal to nil, and some encoders may serialize them differently.
Go’s nil map - safe for reading, panics on write:
| |
Nil maps support all read operations. Looking up missing keys returns the zero value (matching non-nil map behavior). Iteration works (completes immediately). Only mutation requires initialization. This asymmetry is intentional - reading can’t corrupt state, so it’s safe. Writing requires storage, so it requires initialization.
Java’s null - crashes on all operations:
| |
Java’s null is universally invalid. Any method call or field access on null throws NullPointerException. Every null reference forces defensive nil checks throughout the codebase. The absence of an object means the variable is completely unusable.
Why this matters:
In Java, you write defensive code everywhere:
| |
In Go, nil slices work without checks:
| |
The nil check is built into the operation. len(nil) returns 0. This eliminates an entire category of nil checks.
Nil receivers - methods on nil values:
Go allows calling methods on nil receivers if the method handles it:
| |
The method can be called on nil. The method checks if the receiver is nil and handles it. This pattern is impossible in Java (calling methods on null throws NullPointerException) and Python (calling methods on None throws AttributeError).
The typed nil interface trap:
Interfaces add a critical nuance to Go’s nil handling. An interface value consists of two components: a dynamic type and a dynamic value. An interface is nil only when both are nil. This creates a trap:
| |
Interfaces are about the pair (dynamic type, dynamic value). A non-nil dynamic type with a nil dynamic value is not a nil interface. This is a classic Go sharp edge - an interface can be non-nil even though it holds a nil pointer.
The practical impact: when returning interfaces from functions, returning a typed nil pointer creates a non-nil interface. Return an explicit nil interface instead:
| |
Value semantics vs shared backing storage:
The nil distinction maps to Go’s deeper type system, which divides types into two categories based on how assignment and copying work.
Value semantics: When you assign or pass a variable, you copy the entire value. The variable contains the data directly, not a reference to data stored elsewhere. Modifying the copy doesn’t affect the original because they’re independent values occupying separate memory.
| |
Types with value semantics: int, bool, string, arrays, structs. These are never nil - the variable is the value, not a reference to the value.
Shared backing storage: When you assign or pass a variable, you copy a descriptor (slice header, map descriptor, channel descriptor) that references shared underlying storage. Multiple variables can reference the same backing storage. Modifying through one variable affects others because they share the same underlying data.
| |
Types with indirect/descriptor semantics (pointers, slices, maps, channels, interfaces, functions) can be nil because the descriptor can point to no underlying storage.
Go makes this distinction explicit in the type system. Unlike Java (where everything is a reference) or Python (where everything is a reference to an object), Go’s type tells you whether assignment copies the value or copies a reference. This clarity eliminates entire classes of bugs around unexpected sharing.
The design choice:
Go could have made slices and maps work like structs - always allocated, never nil. But that would waste memory (empty map still allocates) and eliminate useful patterns (distinguishing between “not set” and “set to empty”).
Go could have made nil crash on all operations like Java’s null. But that would require defensive nil checks everywhere, defeating the zero value philosophy.
Instead, Go chose a middle ground: nil exists, but it’s a valid zero state with predictable behavior. Types define which operations work on nil. This preserves the zero value philosophy while acknowledging that some types need to represent “not yet allocated.”
Because declaration equals initialization, every variable can be safely used from the moment it’s declared. Value types support all operations. Types with nil zero values support read operations - only mutation requires explicit initialization. This removes entire classes of null-related runtime failures while maintaining Go’s commitment to valid-by-default values.
What Are Zero Values?
Every type has a zero value - the state a variable holds from the moment it’s declared.
For most types, zero values support all operations. For types with nil as their zero value (pointers, slices, maps, channels, interfaces, functions), read operations work but writes may require explicit initialization.
Built-in Types
| Type | Zero Value | Usability |
|---|---|---|
bool | false | Immediately usable |
int, int8, int16, int32, int64 | 0 | Immediately usable |
uint, uint8, uint16, uint32, uint64 | 0 | Immediately usable |
float32, float64 | 0.0 | Immediately usable |
string | "" (empty string) | Immediately usable |
pointer | nil | Safe to check, unsafe to dereference |
slice | nil | Safe to read (length 0), can append |
map | nil | Safe to read, must initialize to write |
channel | nil | Send/receive block forever; len/cap return 0; close(nil) panics |
interface | nil | Safe to compare; calling methods on nil interface panics |
function | nil | Safe to check, unsafe to call |
Example:
| |
Contrast with Other Languages
Python: No Default Values
Python requires explicit initialization or raises NameError:
| |
Java: Split Behavior
Java has different rules for local variables vs fields:
| |
Java objects default to null:
| |
Go: Consistent Zero Values
Go applies zero values uniformly:
| |
Why Zero Values Matter
1. Eliminate Null Pointer Exceptions
Java’s null problem:
| |
Go’s zero value solution:
| |
Value types (int, bool, string, structs) are never nil - they always hold concrete zero values.
2. Simpler Struct Initialization
Python requires boilerplate:
| |
Go’s zero values reduce boilerplate:
| |
3. Enable “Ready to Use” Types
Zero values enable types that work without explicit initialization:
| |
Compare to Java:
| |
Nil Types Summary
Some types have nil as their zero value: pointers, slices, maps, channels, interfaces, and functions. They’re valid zero states with predictable behavior:
- Slices: Reads work;
len/capare 0;appendallocates - Maps: Reads work; writes require
make - Channels: Send/receive block;
len/capare 0;close(nil)panics - Interfaces: Nil only when both dynamic type and value are nil (watch typed nils)
- Functions: Must nil-check before call
Struct Zero Values: Composition
Struct zero values are the zero values of their fields:
| |
Nested structs compose their zero values:
| |
Designing for Zero Values
Pattern 1: Zero Value is Ready to Use
Design types so their zero value is immediately functional:
| |
Pattern 2: Constructor for Complex Setup
When zero value isn’t sufficient, provide a constructor:
| |
Pattern 3: Validate in Methods
Defer initialization until first use:
| |
Comparison: Initialization Patterns
Python: Explicit Initialization Required
| |
Java: Constructors or Null
| |
Go: Zero Value Composability
| |
Zero Values and Memory Safety
Zero values make Go’s memory model predictable:
| |
Contrast with C (uninitialized memory):
| |
Contrast with Java (null references):
| |
Go guarantees: Variables are always initialized to their zero value. No uninitialized memory, no accidental null dereferences for value types.
When Zero Values Don’t Suffice
Zero values work when the default state is genuinely useful - an empty string, a count of zero, an unlocked mutex. But not all types have a meaningful default. Some types exist to wrap external resources (database connections, file handles). Others represent domain concepts that require specific values to be valid (email addresses, API keys). Still others need configuration before they can do anything useful (HTTP clients, loggers).
For these types, the zero value exists but isn’t usable. An HTTP client with no endpoint can’t make requests. A database wrapper with no connection can’t query. An email address that’s an empty string violates business logic.
When zero values don’t suffice, Go provides constructors - functions (typically named New*) that return properly initialized values. This preserves Go’s zero value model while acknowledging that some types need explicit setup.
The decision comes down to: Can this type do something useful with all fields set to their zero values? If yes, make it work. If no, require a constructor.
Requires Configuration
| |
Requires External Resources
| |
Requires Validation
| |
The Standard Library’s Approach
Go’s standard library demonstrates zero value design:
sync.Mutex: Zero Value Ready
| |
bytes.Buffer: Zero Value Ready
| |
http.Server: Constructor Required
| |
The pattern: If a type can be useful with zero values, make it so. If it requires configuration or external resources, provide a constructor (New* function).
Putting It Together
Go’s zero value philosophy stems directly from its value model. In languages where variables are references to objects, uninitialized variables either error (Python) or hold null (Java). In Go, where variables are values, uninitialized variables hold the zero value of their type.
This creates a programming model where declaration equals initialization. No separate steps, no null checks for value types, no uninitialized memory. Every variable is immediately valid and safe to use, even if not explicitly initialized.
The tradeoffs:
Python’s explicit initialization prevents accidentally using uninitialized state but requires boilerplate constructors. Java’s null defaults enable lazy initialization but introduce null pointer exceptions. Go’s zero values provide safety and simplicity but require thoughtful API design to ensure zero values are actually useful.
The mental model: In Go, absence of explicit initialization doesn’t mean “uninitialized” or “null.” It means “initialized to the most reasonable default for this type.” This shifts error handling from defensive nil checks to validating business logic instead.
Further Reading
Go Initialization:
Related Posts:
Next in Series
Part 4: Slices, Maps, and Channels - The Hybrid Types - Coming soon. Learn why these types look like values but behave like references, and how this affects your code.
📚 Series: Go Value Philosophy
- Go's Value Philosophy: Part 1 - Why Everything Is a Value, Not an Object
- Go's Value Philosophy: Part 2 - Escape Analysis and Performance
- Go's Value Philosophy: Part 3 - Zero Values: Go's Valid-by-Default Philosophy (current)