Zero Values — Middle Level¶
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Evolution & Historical Context
- Real-World Analogies
- Mental Models
- Pros & Cons
- Alternative Approaches / Plan B
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Product Use / Feature
- Error Handling
- Security Considerations
- Performance Tips
- Metrics & Analytics
- Best Practices
- Anti-Patterns
- Debugging Guide
- Edge Cases & Pitfalls
- Common Mistakes
- Common Misconceptions
- Tricky Points
- Comparison with Other Languages
- Test
- Tricky Questions
- Cheat Sheet
- Self-Assessment Checklist
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: "Why?" and "When to use?"
Zero values in Go aren't just a safety feature — they are a design philosophy. Go forces you to think about what "nothing" or "empty" means for each type you design. When you understand zero values deeply, you write APIs and data structures that are instantly usable without configuration.
The question a mid-level Go developer should always ask: "Is the zero value of this type useful and safe?" If yes, your API is clean. If no, you need to add a constructor or change the design.
This level covers: - Why the zero value design decision was made - How to leverage zero values in API design - The subtle difference between nil and empty - JSON serialization behavior - How sync.Mutex and bytes.Buffer use zero values as a design pattern
Prerequisites¶
- Solid understanding of all Go types
- Experience with struct design
- Familiarity with interfaces
- Basic understanding of JSON marshaling in Go
- Understanding of Go's concurrency primitives (Mutex concepts)
Glossary¶
| Term | Definition |
|---|---|
| Zero value pattern | Designing a type so its zero value represents a valid, ready-to-use state |
| nil slice | A slice variable declared without allocation (var s []int) — type is set, no backing array |
| empty slice | A slice with a backing array but zero elements (s := []int{}) |
| omitempty | JSON struct tag that omits fields equal to their zero value during serialization |
| Nil interface | An interface where both the type and value are nil |
| Non-nil nil | An interface with a nil concrete value but a non-nil type — a common Go gotcha |
| Sentinel value | A special value used to represent "not set" or "end of data" |
Core Concepts¶
The Zero Value Contract¶
Go's specification states: "Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for integers, 0.0 for floats, "" for strings, and nil for pointers, functions, interfaces, slices, channels, and maps."
This is a contract — guaranteed by the language spec, not an implementation detail.
nil vs Zero Value: The Distinction¶
nil IS the zero value for reference types, but nil and "zero value" are not synonyms:
// These are zero values but NOT nil:
var i int // 0 (not nil, not nilable)
var s string // "" (not nil, not nilable)
var b bool // false (not nil, not nilable)
// These are zero values AND nil:
var p *int // nil
var sl []int // nil
var m map[string]int // nil
nil Slice vs Empty Slice¶
This is one of the most important distinctions in Go:
var nilSlice []int // nil == true, len == 0, cap == 0
emptySlice := []int{} // nil == false, len == 0, cap == 0
// Behavior is nearly identical:
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
// But they differ in nil check:
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
// And in JSON marshaling:
import "encoding/json"
n, _ := json.Marshal(nilSlice) // null
e, _ := json.Marshal(emptySlice) // []
nil Map Asymmetry¶
var m map[string]int
// Reading: SAFE — returns zero value
v := m["key"] // v = 0, no panic
v, ok := m["key"] // v = 0, ok = false, no panic
// Writing: PANIC
m["key"] = 1 // panic: assignment to entry in nil map
// Iteration: SAFE — iterates 0 times
for k, v := range m { // safe, never executes body
_ = k
_ = v
}
The Zero Value Pattern in Standard Library¶
Go's standard library was designed around usable zero values:
// sync.Mutex: zero value is unlocked — ready to use
var mu sync.Mutex
mu.Lock() // works immediately without New() or init
mu.Unlock()
// bytes.Buffer: zero value is empty buffer — ready to use
var buf bytes.Buffer
buf.WriteString("hello") // works immediately
fmt.Println(buf.String()) // "hello"
// sync.WaitGroup: zero value is "no goroutines to wait for"
var wg sync.WaitGroup
wg.Add(1)
go func() { wg.Done() }()
wg.Wait()
Evolution & Historical Context¶
Why Go Made This Choice¶
Before Go, languages took different approaches:
- C: No initialization — memory contains whatever bits were there previously (garbage). This is fast but dangerous.
- Java: Instance variables are initialized to defaults (
0,false,null), but local variables are NOT. The compiler flags uninitialized local variables. - C++: Same split as Java, with the added complexity of uninitialized member variables if constructors don't set them.
- Python/Ruby: Everything is an object with a defined state, but
Noneassignment is manual.
Go's Design Decision (2009–2012)¶
Rob Pike and the Go team decided that all variables must always be initialized. The reasoning: 1. Eliminates a whole class of bugs 2. Makes programs deterministic — same source always produces same initial state 3. Simplifies the mental model — no special rules for "sometimes initialized" 4. The cost (zeroing memory) is minimal since the OS often provides zero pages anyway
The Impact on API Design¶
This decision shaped how Gophers design APIs. The canonical example is sync.Mutex:
// Old C-style approach (NOT Go style):
mu := NewMutex() // required constructor
defer mu.Destroy() // required cleanup
// Go style (zero value pattern):
var mu sync.Mutex // just declare it
mu.Lock() // immediately usable
This pattern is now called the "zero value is useful" design principle and is the standard for Go library design.
Real-World Analogies¶
Analogy 1: Empty Inbox vs No Inbox¶
var msgs []Message(nil slice): like having a mailbox that hasn't been installed yet, but you can still put letters in it and they materialize the mailbox.msgs := []Message{}(empty slice): like having an installed, empty mailbox. For most purposes they're the same, but they look different in JSON.
Analogy 2: Unlocked Door (sync.Mutex)¶
A new sync.Mutex is like a door that starts unlocked. You can immediately lock/unlock it without any setup ceremony.
Analogy 3: Template in a CMS¶
A nil string in a config struct is like a template field that says "use the default template" — the zero value carries semantic meaning (use default).
Mental Models¶
Model 1: The Three Questions¶
When you see nil in Go, ask: 1. Is this a pointer? → "Points to nothing, don't dereference" 2. Is this a slice/map/chan? → "Empty container, handle before writing to map" 3. Is this an interface? → "Careful! Type might be set even if value is nil"
Model 2: The Nil Flow Diagram¶
variable declared
|
v
nilable type?
/ \
YES NO
| |
nil zero value
| (0, "", false)
|
|-- read? --> returns zero value (safe)
|-- write map? --> PANIC
|-- deref pointer? --> PANIC
|-- append slice? --> OK (creates new backing array)
|-- range? --> OK (0 iterations)
Model 3: Interface nil vs concrete nil¶
// This is a common trap:
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mayFail(fail bool) error {
var err *MyError // typed nil
if fail {
err = &MyError{"something went wrong"}
}
return err // DANGER: returns non-nil interface even when err is nil!
}
// Check:
e := mayFail(false)
fmt.Println(e == nil) // false! interface is not nil even though *MyError is nil
Pros & Cons¶
Pros¶
- Eliminates uninitialized memory bugs — a massive improvement over C/C++
- Simpler API design — zero value pattern means fewer constructors needed
- Consistent behavior — predictable initial state everywhere
- Better for concurrent code —
sync.Mutex{}zero value is immediately usable - Standard library consistency —
bytes.Buffer,sync.WaitGroup, etc. all follow this pattern
Cons¶
- "Not set" ambiguity — can't distinguish "value is 0" from "value was never set"
- nil map write panic — the asymmetry between read (safe) and write (panic) is surprising
- Interface nil trap — typed nil interface values are not equal to untyped nil
- JSON zero values — zero values serialize to JSON, which can leak defaults or require
omitempty - Pointer semantics required for "optional" — must use
*intto express "optional integer"
Alternative Approaches / Plan B¶
When Zero Value Isn't Enough: Use a Pointer¶
// Problem: can't tell if age was set to 0 or never set
type Profile struct {
Age int
}
// Solution: use pointer for optional fields
type Profile struct {
Age *int // nil = not set, &0 = explicitly set to 0
}
// Usage:
age := 0
p := Profile{Age: &age} // explicitly set to 0
q := Profile{} // not set
When Zero Value Isn't Enough: Use a Sentinel¶
// Use -1 or some sentinel value for "not set"
type Score struct {
Value int // -1 = not set, >= 0 = actual score
}
const ScoreNotSet = -1
func (s Score) IsSet() bool {
return s.Value != ScoreNotSet
}
When Zero Value Isn't Enough: Use a Boolean Flag¶
type OptionalInt struct {
Value int
Valid bool // false = not set
}
// Similar to sql.NullInt64 pattern
type NullInt64 struct {
Int64 int64
Valid bool
}
When Zero Value Isn't Enough: Use a Constructor¶
type Cache struct {
data map[string]string
maxSize int
}
// Zero value is NOT usable (nil map)
// Use a constructor:
func NewCache(maxSize int) *Cache {
return &Cache{
data: make(map[string]string),
maxSize: maxSize,
}
}
Use Cases¶
Use Case 1: API Config with Defaults¶
type APIConfig struct {
BaseURL string
Timeout int // 0 = use 30s default
MaxRetries int // 0 = use 3 retries default
Debug bool // false = production mode
RateLimit int // 0 = no rate limit
}
func (c *APIConfig) defaults() {
if c.BaseURL == "" {
c.BaseURL = "https://api.example.com"
}
if c.Timeout == 0 {
c.Timeout = 30
}
if c.MaxRetries == 0 {
c.MaxRetries = 3
}
}
Use Case 2: Lazy Initialization with nil Check¶
type Service struct {
cache map[string][]byte
}
func (s *Service) getFromCache(key string) ([]byte, bool) {
if s.cache == nil {
return nil, false // not initialized yet
}
v, ok := s.cache[key]
return v, ok
}
func (s *Service) setCache(key string, val []byte) {
if s.cache == nil {
s.cache = make(map[string][]byte)
}
s.cache[key] = val
}
Use Case 3: JSON with omitempty¶
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if ""
Bio string `json:"bio,omitempty"` // omit if ""
Premium bool `json:"premium"` // always include
Score *int `json:"score,omitempty"` // omit if nil
}
Code Examples¶
Example 1: Zero Value Pattern in Library Design¶
package counter
// Counter uses the zero value pattern:
// var c Counter works without initialization
type Counter struct {
value int
mu sync.Mutex // zero value is unlocked
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
// Usage:
// var c counter.Counter -- works immediately!
// c.Increment()
Example 2: nil vs empty slice in JSON¶
package main
import (
"encoding/json"
"fmt"
)
type Response struct {
Items []string `json:"items"`
Tags []string `json:"tags,omitempty"`
}
func main() {
// nil slice in Items -> null in JSON
// nil slice in Tags (omitempty) -> omitted from JSON
r1 := Response{}
b1, _ := json.Marshal(r1)
fmt.Println(string(b1))
// {"items":null}
// empty slice in Items -> [] in JSON
r2 := Response{Items: []string{}, Tags: []string{}}
b2, _ := json.Marshal(r2)
fmt.Println(string(b2))
// {"items":[],"tags":[]}
}
Example 3: Interface nil trap¶
package main
import "fmt"
type Error interface {
Error() string
}
type MyError struct {
msg string
}
func (e *MyError) Error() string { return e.msg }
func getError(fail bool) error {
var e *MyError // typed nil
if fail {
e = &MyError{"oops"}
}
// BAD: returning typed nil as interface — interface won't be nil!
return e
}
func main() {
err := getError(false)
if err != nil {
fmt.Println("Error:", err) // This WILL print even though *MyError is nil!
}
}
Fix:
func getError(fail bool) error {
if fail {
return &MyError{"oops"}
}
return nil // return untyped nil, not *MyError(nil)
}
Example 4: Checking Struct for Zero Value¶
package main
import "fmt"
type Point struct {
X, Y float64
}
func main() {
var p Point // zero value: {0, 0}
zero := Point{}
fmt.Println(p == zero) // true
fmt.Println(p == (Point{0, 0})) // true
p.X = 1.0
fmt.Println(p == zero) // false
}
Example 5: new() and zero values¶
package main
import "fmt"
func main() {
// new() allocates zeroed memory and returns a pointer
p := new(int)
fmt.Println(*p) // 0
fmt.Println(p == nil) // false — p points to a valid zero int
s := new(struct {
Name string
Age int
})
fmt.Println(s.Name) // ""
fmt.Println(s.Age) // 0
}
Coding Patterns¶
Pattern 1: Functional Options with Zero Values¶
type ServerOptions struct {
Host string
Port int
MaxConns int
TLSEnabled bool
}
type Option func(*ServerOptions)
func WithHost(h string) Option {
return func(o *ServerOptions) { o.Host = h }
}
func WithPort(p int) Option {
return func(o *ServerOptions) { o.Port = p }
}
func NewServer(opts ...Option) *Server {
o := &ServerOptions{
Host: "localhost", // override zero value default
Port: 8080,
MaxConns: 100,
}
for _, opt := range opts {
opt(o)
}
return &Server{options: o}
}
Pattern 2: Lazy Map Initialization¶
type Registry struct {
handlers map[string]Handler
}
func (r *Registry) Register(name string, h Handler) {
if r.handlers == nil {
r.handlers = make(map[string]Handler)
}
r.handlers[name] = h
}
func (r *Registry) Get(name string) (Handler, bool) {
h, ok := r.handlers[name] // safe even if nil
return h, ok
}
Pattern 3: Builder with Zero Value Start¶
type QueryBuilder struct {
table string
conditions []string
orderBy string
limit int
}
func (q *QueryBuilder) From(table string) *QueryBuilder {
q.table = table
return q
}
func (q *QueryBuilder) Where(cond string) *QueryBuilder {
q.conditions = append(q.conditions, cond) // nil slice is fine
return q
}
func (q *QueryBuilder) OrderBy(field string) *QueryBuilder {
q.orderBy = field
return q
}
// Usage: var qb QueryBuilder; qb.From("users").Where("active=true")
Clean Code¶
Do: Make zero value represent "empty/default" state¶
// Good: zero value means "empty event"
type Event struct {
Type string // "" = no type
Payload []byte // nil = no payload
Sent bool // false = not sent
}
// Good: zero value means "no result yet"
type Result struct {
Value int
Error error
Done bool // false = not complete
}
Do: Use sync.Mutex without initialization¶
// Good
type SafeMap struct {
mu sync.Mutex
data map[string]int
}
// Bad: unnecessary initialization
type SafeMap struct {
mu *sync.Mutex // pointer is unnecessary
data map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{mu: &sync.Mutex{}} // over-engineered
}
Product Use / Feature¶
Feature: Rate Limiter Using Zero Values¶
type RateLimiter struct {
mu sync.Mutex // zero value = unlocked
count int // zero value = 0 requests
window time.Duration // zero value = 0, normalized to default
maxCount int // zero value = 0, normalized to default
lastReset time.Time // zero value = time.Time{}
}
func (r *RateLimiter) Allow() bool {
r.mu.Lock()
defer r.mu.Unlock()
if r.maxCount == 0 {
r.maxCount = 100 // default
}
if r.window == 0 {
r.window = time.Minute
}
now := time.Now()
if r.lastReset.IsZero() || now.Sub(r.lastReset) > r.window {
r.count = 0
r.lastReset = now
}
if r.count >= r.maxCount {
return false
}
r.count++
return true
}
Error Handling¶
Pattern: Return nil on no-error, non-nil on error¶
func processUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user id: %d", id)
}
user := &User{ID: id}
// ... process
return user, nil // nil error = success
}
// Check:
user, err := processUser(42)
if err != nil {
log.Printf("error: %v", err)
return
}
// user is valid here
Pattern: error interface nil trap (avoid this)¶
// DANGEROUS: typed nil
func validate(s string) error {
var ve *ValidationError // nil pointer to ValidationError
if s == "" {
ve = &ValidationError{"empty string"}
}
return ve // ALWAYS returns non-nil interface!
}
// SAFE: return explicit nil
func validate(s string) error {
if s == "" {
return &ValidationError{"empty string"}
}
return nil // untyped nil — interface is nil
}
Security Considerations¶
Zero Values and Sensitive Data¶
// GOOD: zero value for token is "" — safe default
type Session struct {
Token string
UserID int
IsAdmin bool // false = not admin — safe default!
}
// DANGER: If your system uses zero value = "admin":
type BadDesign struct {
Role int // 0 = admin?! That's the zero value — dangerous default
}
// BETTER: use explicit constants
const (
RoleGuest = 0
RoleUser = 1
RoleAdmin = 2
)
// OR use a string:
type GoodDesign struct {
Role string // "" = not set, "user" = regular user, "admin" = admin
}
Performance Tips¶
nil Slice vs make for Known Size¶
// If you don't know the size, nil slice + append is fine:
var results []string
for _, item := range input {
if item.Valid {
results = append(results, item.Name)
}
}
// If you know the size, pre-allocate:
results := make([]string, 0, len(input))
for _, item := range input {
if item.Valid {
results = append(results, item.Name)
}
}
// Avoids multiple reallocations
sync.Mutex Zero Value Avoids Allocation¶
// Bad: allocates on heap
mu := new(sync.Mutex)
// Good: stack-allocated, zero value works
var mu sync.Mutex
// In a struct (both work, but value is more cache-friendly):
type Good struct {
mu sync.Mutex // embedded by value
// ...
}
type Okay struct {
mu *sync.Mutex // pointer — extra indirection, extra allocation
}
Metrics & Analytics¶
type AnalyticsEvent struct {
EventName string // "" = unset
UserID int // 0 = anonymous
SessionID string // "" = no session
Properties map[string]interface{} // nil = no properties
Timestamp time.Time // zero = use time.Now() when sending
Count int // 0 = 1 occurrence (default)
}
func (e *AnalyticsEvent) normalize() {
if e.Timestamp.IsZero() {
e.Timestamp = time.Now()
}
if e.Count == 0 {
e.Count = 1
}
}
Best Practices¶
- Design for usable zero values: Ask "does the zero value make sense?" when creating structs.
- Use
sync.Mutexby value, not pointer: Zero value pattern makes pointer unnecessary. - Prefer nil slice for "no results": More idiomatic than returning
[]T{}. - Return explicit
nilfrom error functions: Never return typed nil as an error interface. - Use
omitemptyin JSON tags when zero value shouldn't appear in output. - Check maps for nil before writing: Use lazy initialization pattern.
- Use
new(T)to get a pointer to zero-valued T: Cleaner thanvar x T; return &x.
Anti-Patterns¶
Anti-Pattern 1: Checking if nil slice == empty¶
// Wrong: this check is unnecessary
if results == nil || len(results) == 0 {
// handle empty
}
// Right: len handles nil gracefully
if len(results) == 0 {
// handles both nil and empty slice
}
Anti-Pattern 2: Returning typed nil as error¶
// ANTI-PATTERN
func doWork() error {
var err *MyError
// ... if no error, return nil MyError pointer
return err // This is a non-nil interface!
}
// CORRECT
func doWork() error {
// ... if no error
return nil // explicit untyped nil
}
Anti-Pattern 3: Initializing to zero explicitly¶
// Verbose anti-pattern
type Config struct {
Debug bool
Retries int
}
c := Config{
Debug: false, // unnecessary
Retries: 0, // unnecessary
}
// Clean
c := Config{} // or even: var c Config
Anti-Pattern 4: Unnecessary pointer for mutex¶
// Anti-pattern
type Worker struct {
mu *sync.Mutex // unnecessary pointer
}
func NewWorker() *Worker {
return &Worker{mu: &sync.Mutex{}}
}
// Clean
type Worker struct {
mu sync.Mutex // zero value works perfectly
}
Debugging Guide¶
Debugging nil Pointer Panics¶
Steps: 1. Look at the stack trace to identify which line panicked 2. Check what variable was nil on that line 3. Trace back to where the variable was declared/assigned 4. Add nil check OR ensure the variable is initialized before use// Debug: print before dereference
fmt.Printf("pointer is nil: %v\n", p == nil)
if p == nil {
log.Fatal("unexpected nil pointer at line X")
}
fmt.Println(*p)
Debugging nil Map Panics¶
// Add at the beginning of a function that modifies a map:
if m == nil {
log.Printf("map is nil, initializing")
m = make(map[string]int)
}
Debugging Interface nil Confusion¶
// Add explicit type printing to understand interface state
var err error = getError()
fmt.Printf("err type: %T, err value: %v, err == nil: %v\n",
err, err, err == nil)
Edge Cases & Pitfalls¶
Pitfall 1: time.Time zero value¶
var t time.Time
fmt.Println(t) // 0001-01-01 00:00:00 +0000 UTC
fmt.Println(t.IsZero()) // true — use IsZero() not t == time.Time{}
Pitfall 2: Struct with non-comparable fields¶
type BadStruct struct {
Data []int // slices are not comparable
}
var a, b BadStruct
// a == b // COMPILE ERROR: struct contains non-comparable field
Pitfall 3: nil channel operations¶
var ch chan int
// Sending to nil channel blocks forever
// go func() { ch <- 1 }() // goroutine leak
// Receiving from nil channel blocks forever
// val := <-ch // blocks forever
// But: nil channel in select is simply ignored
select {
case v := <-ch: // never selected if ch is nil
fmt.Println(v)
default:
fmt.Println("no message")
}
Pitfall 4: Copy of sync.Mutex¶
type Worker struct {
mu sync.Mutex
data string
}
// WRONG: copying Worker copies the Mutex — undefined behavior!
w1 := Worker{data: "hello"}
w2 := w1 // copies mutex!
// RIGHT: use pointer receiver or pointer to struct
w1 := &Worker{data: "hello"}
// pass w1 around by pointer
Common Mistakes¶
Mistake 1: Using zero value for missing optional config¶
// Problem: 0 port is ambiguous
type Config struct {
Port int // 0 could mean "not set" or "use default" or "port 0"
}
// Better:
type Config struct {
Port *int // nil = not set, &0 = explicitly 0
}
Mistake 2: Not using IsZero() for time.Time¶
// Wrong
var t time.Time
if t == (time.Time{}) { /* zero check */ }
// Right — use the provided method
if t.IsZero() { /* zero check */ }
Common Misconceptions¶
Misconception: nil means "uninitialized" or "broken"¶
nil is a valid, defined zero value. A nil slice, nil error, nil pointer — these are valid states that carry meaning.
Misconception: nil interface == nil pointer¶
var p *MyError = nil
var err error = p // err is NOT nil — the interface has a type!
fmt.Println(err == nil) // false — surprising but correct
Tricky Points¶
time.Timezero value is0001-01-01 00:00:00 UTC— uset.IsZero()to check, nott == time.Time{}(though both work).sync.Mutexmust not be copied after first use — even though its zero value works, copying a locked mutex is a bug.- nil interface vs nil pointer — an interface holding a nil pointer is NOT nil.
new(T)vs&T{}— both give a pointer to a zero-valued T; they are equivalent.- Struct fields with maps — if a struct has a map field, that field is nil by default and must be initialized before use.
Comparison with Other Languages¶
| Feature | Go | Java | C | Python | Rust |
|---|---|---|---|---|---|
| Variable init | Always zero | Varies | Never | N/A | Compile error if used uninit |
| Null safety | nil exists | null exists | N/A | None exists | Option |
| Uninitialized memory | Impossible | Local vars may be | Common | N/A | Impossible |
| Default struct values | Zero values | All fields default | Garbage | N/A | Must impl Default |
| nil/null write safe? | Map write panics | NullPointerException | Undefined behavior | AttributeError | N/A |
Go vs Java¶
- Java: instance fields get default values (0, false, null), but local variables must be explicitly initialized
- Go: ALL variables (local and fields) get zero values — consistent rule
Go vs C¶
- C: uninitialized memory contains garbage — source of endless security vulnerabilities
- Go: all memory zeroed — no garbage values ever
Go vs Rust¶
- Rust: compiler prevents use of uninitialized variables at compile time
- Go: runtime guarantees all variables are initialized to zero
- Rust requires explicit
Defaulttrait for "default" values; Go provides it automatically
Test¶
Q1: What does json.Marshal produce for a nil slice vs empty slice? - A) Both produce null - B) nil → null, empty → [] - C) Both produce [] - D) nil is omitted, empty → []
Answer: B
Q2: You have var mu sync.Mutex. Can you call mu.Lock() immediately? - A) No, you need mu = sync.Mutex{} first - B) No, you need mu = new(sync.Mutex) first - C) Yes, zero value of sync.Mutex is unlocked - D) It depends on the Go version
Answer: C
Q3: What is the result of this code?
- A)true - B) false - C) Compile error - D) Panic Answer: B
Tricky Questions¶
Q: Can you copy a sync.Mutex? Technically yes (it's a value type), but you must NEVER copy it after first use. The go vet tool catches this.
Q: What is new(sync.Mutex) vs var mu sync.Mutex? new returns *sync.Mutex (pointer to zeroed Mutex). var mu sync.Mutex gives you a value. Both work; use value unless you need pointer semantics.
Q: Why does nil map read return zero value but write panics? This is a deliberate design choice. Reading is always safe (zero value is well-defined). Writing requires a backing hash table structure that doesn't exist for nil. The inconsistency is considered a minor design wart by the Go team.
Cheat Sheet¶
nil slice: var s []T
- s == nil: true
- len(s): 0
- append(s, x): OK
- range s: OK (0 iters)
- json: null
empty slice: s := []T{}
- s == nil: false
- len(s): 0
- append(s, x): OK
- json: []
nil map: var m map[K]V
- m["k"]: returns zero value (safe)
- m["k"] = v: PANIC
- range m: OK (0 iters)
- len(m): 0
sync.Mutex zero value: unlocked, ready to use
bytes.Buffer zero value: empty buffer, ready to use
sync.WaitGroup zero value: zero counter, ready to use
Interface nil trap:
var p *T = nil
var i Interface = p
i == nil -> false (type is set)
Self-Assessment Checklist¶
- I understand the difference between nil slice and empty slice
- I know when nil map reads are safe and when writes panic
- I can design a struct with a usable zero value
- I understand the interface nil trap
- I know why sync.Mutex zero value is unlocked
- I can use
omitemptycorrectly for JSON serialization - I understand why typed nil != untyped nil for interfaces
- I know the alternative patterns when zero value isn't enough
Summary¶
At the middle level, zero values become a design tool: - Ask: "Is the zero value of my type useful?" - Know: nil slice vs empty slice have different JSON representations but similar behavior - Avoid: the interface nil trap — return explicit nil, not typed nil pointers - Use: sync.Mutex zero value pattern for clean concurrent types - Apply: omitempty to control JSON serialization of zero-value fields
What You Can Build¶
- Thread-safe counters and caches using
sync.Mutexzero value - JSON APIs with proper
omitemptyhandling - Lazy-initialized data structures
- Builder patterns starting from zero value
- Optional field types (sql.NullString pattern)
Further Reading¶
- Go Specification: The zero value
- Go Blog: JSON and Go
- Effective Go: Constructors and composite literals
- Dave Cheney: The zero value is an important Go design principle
Related Topics¶
sync.Mutexand concurrent types- JSON marshaling/unmarshaling
- Pointer semantics
- Interface design
- Error handling patterns
- Functional options pattern
bytes.Bufferand io interfaces
Diagrams & Visual Aids¶
nil vs Empty Slice Memory Layout¶
var s []int (nil slice)
┌──────────────────────────────┐
│ ptr: nil len: 0 cap: 0 │ <- no backing array
└──────────────────────────────┘
s := []int{} (empty slice)
┌──────────────────────────────┐
│ ptr: 0xABC len: 0 cap: 0 │ <- points to zerosize alloc
└──────────────────────────────┘
│
v
[backing array - empty]
Interface nil Anatomy¶
Interface Value:
┌─────────────┬─────────────┐
│ type ptr │ data ptr │
└─────────────┴─────────────┘
nil interface:
┌─────────────┬─────────────┐
│ nil │ nil │ <- both nil
└─────────────┴─────────────┘
typed nil interface (NOT nil):
┌─────────────┬─────────────┐
│ *MyError │ nil │ <- type is set!
└─────────────┴─────────────┘