Zero Values and Initialization
Every Go type has a zero value: the value a variable holds before you assign anything.
Designing with zero values in mind leads to simpler APIs and fewer nil checks.
Summary
Declaring with var or allocating with new/make without extra data yields the type's zero value.
Numeric types are 0, bool is false, strings are "", and reference types (pointers, slices, maps, channels, functions, interfaces) are nil.
Idiomatic Go often makes the zero value useful so callers can zero-initialize a struct and use it immediately.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import "fmt"
type Config struct {
Host string
Port int
}
func main() {
var cfg Config // zero: Host "", Port 0
cfg.Host = "localhost"
cfg.Port = 8080
nums := make([]int, 0, 8) // empty slice, non-nil
fmt.Println(cfg, nums == nil)
}When to reach for this:
- Prefer zero-initialized structs when defaults are safe (
sync.Mutex, many config structs). - Use
makefor slices, maps, and channels when you need a non-nilready-to-use value. - Use composite literals
T{...}when defaults are not enough. - Document when zero is invalid so callers know to set required fields.
Working Example
package main
import (
"bytes"
"fmt"
)
type Buffer struct {
buf bytes.Buffer
ok bool
}
func (b *Buffer) WriteString(s string) {
if !b.ok {
b.buf = *bytes.NewBuffer(nil)
b.ok = true
}
b.buf.WriteString(s)
}
func (b Buffer) String() string {
if !b.ok {
return ""
}
return b.buf.String()
}
func main() {
var b Buffer // zero value usable: String() returns ""
b.WriteString("hello")
fmt.Println(b.String())
m := make(map[string]int)
m["a"] = 1
var sl []int // nil slice
sl = append(sl, 2)
fmt.Println(m, sl)
}What this demonstrates:
- Struct zero value is usable with helper methods guarding lazy init.
make(map[K]V)returns an empty, non-nilmap ready for assignment.nilslices work withappendandlen/capreturn 0.- Embedding
bytes.Bufferwould change zero-value behavior - design fields deliberately.
Deep Dive
How It Works
var x Tallocates storage and sets bits to zero for typeT.new(T)returns*Tpointing to zero-initialized storage (rarely needed vs&T{}).makeonly applies to slices, maps, and channels; it allocates internal structure.- Composite literals
T{v}or&T{v}set fields explicitly while leaving others at zero.
Zero Values by Type
| Type kind | Zero value | Usable without init? |
|---|---|---|
int, float, complex | 0 | Yes |
bool | false | Yes |
string | "" | Yes |
pointer, func, interface | nil | Only after nil checks |
slice | nil | Yes for len, append, range |
map | nil | No for assignment - panics |
channel | nil | No for send/receive - blocks or panics |
struct | each field zero | Depends on field types |
Initialization Patterns
| Pattern | Example | When |
|---|---|---|
| Zero + set fields | var c Config; c.Port = 8080 | Few fields, safe defaults |
| Composite literal | Config{Host: "x"} | Known values at creation |
make | make([]int, 0, n) | Preallocate capacity |
| Constructor func | NewServer(cfg) | Validation or invariants required |
Go Notes
import "sync"
// Mutex zero value is an unlocked mutex - ready to use
var mu sync.Mutex
mu.Lock()
// Prefer &T{} over new(T) for structs when you might add fields later
srv := &Server{}Gotchas
- Writing to nil map -
var m map[string]int; m["k"] = 1panics. Fix:m = make(map[string]int)or a map literal before assignment. - Sending on nil channel - Blocks forever in select or deadlocks. Fix:
make(chan T)or guard with nil check. - Nil interface vs typed nil -
var p *T; var i interface{} = pmakesi != niltrue whilep == nil. Fix: return concrete types or check typed nil explicitly. - Expecting zero port to mean "random" - Zero
intmay be invalid for networking APIs. Fix: use pointers*intornet.JoinHostPortwith validation. - Slice zero vs empty literal -
var s []intands := []int{}differ for JSON (nullvs[]). Fix: pick intentionally for serialization semantics. newconfusion -new(T)returns*Tto zeros; many style guides prefer&T{}for structs. Fix: use composite literals for clarity.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Zero-value struct | Mutexes, simple configs | Required fields have no sensible default |
make + append | Building slices dynamically | Fixed small arrays suffice |
Pointer fields *int | Distinguish unset from zero | Every field would become a pointer (noise) |
| Builder/constructor | Complex validation | Simple structs with 2-3 fields |
FAQs
What is the zero value of a struct?
Each field is set to its own zero value recursively.
For struct{ A int; B string }, that is 0 and "".
Is a nil slice the same as an empty slice?
Both have len 0 and work with append.
They differ for == nil, JSON encoding, and some APIs that treat nil specially.
When must I use make instead of var?
Use make when you need a non-nil map, channel, or a slice with specific len/cap.
var s []int is fine when you will only append.
Does new allocate on the heap?
new(T) returns a pointer to zeroed storage.
Escape analysis decides stack vs heap; new does not force heap allocation by itself.
Can I compare structs containing slices?
Structs are comparable only if all fields are comparable.
Slices, maps, and functions make structs non-comparable.
What is the zero value of an interface?
nil interface value has no dynamic type or value.
A typed nil pointer assigned to an interface is not equal to nil interface.
How do array zero values differ from slices?
var a [3]int is three zeros on the stack or in the struct.
var s []int is a nil header with no backing array until make or append.
Should constructors always replace zero values?
Only when invariants are required (non-empty name, positive port).
Many stdlib types (bytes.Buffer, sync.Mutex) are designed for zero-value use.
What about zero values for time.Time?
Zero time.Time is January 1, year 1 UTC - rarely a useful sentinel.
Use pointers or time.Time.IsZero() checks.
How do I initialize nested structs?
type Outer struct{ Inner Inner }
o := Outer{Inner: Inner{Field: 1}}Unmentioned nested fields remain zero inside Inner.
Are map and slice headers copied on assignment?
Assignment copies the header; backing store is shared.
Initialize once and pass carefully to avoid unintended sharing.
Does Go zero memory for security?
Go zeros allocations for safety in many paths, but do not rely on that for crypto secrets.
Overwrite sensitive buffers explicitly when needed.
Related
- Variables, Constants & Scope -
varvs:=at declaration time - Arrays, Slices & Maps at a Glance - nil vs empty collections
- Pointers & the Address-of Operator - nil pointers and initialization
- Structs, Embedding & Field Promotion - struct literal patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).