Zero Values and Initialization
Every Go type has a zero value: the value a variable holds before you assign anything.
Search across all documentation pages
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.
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.
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:
sync.Mutex, many config structs).make for slices, maps, and channels when you need a non-nil ready-to-use value.T{...} when defaults are not enough.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:
make(map[K]V) returns an empty, non-nil map ready for assignment.nil slices work with append and len/cap return 0.bytes.Buffer would change zero-value behavior - design fields deliberately.var x T allocates storage and sets bits to zero for type T.new(T) returns *T pointing to zero-initialized storage (rarely needed vs &T{}).make only applies to slices, maps, and channels; it allocates internal structure.T{v} or &T{v} set fields explicitly while leaving others at zero.| 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 |
| 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 |
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{}var m map[string]int; m["k"] = 1 panics. Fix: m = make(map[string]int) or a map literal before assignment.make(chan T) or guard with nil check.var p *T; var i interface{} = p makes i != nil true while p == nil. Fix: return concrete types or check typed nil explicitly.int may be invalid for networking APIs. Fix: use pointers *int or net.JoinHostPort with validation.var s []int and s := []int{} differ for JSON (null vs []). Fix: pick intentionally for serialization semantics.new confusion - new(T) returns *T to zeros; many style guides prefer &T{} for structs. Fix: use composite literals for clarity.| 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 |
Each field is set to its own zero value recursively.
For struct{ A int; B string }, that is 0 and "".
Both have len 0 and work with append.
They differ for == nil, JSON encoding, and some APIs that treat nil specially.
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.
new(T) returns a pointer to zeroed storage.
Escape analysis decides stack vs heap; new does not force heap allocation by itself.
Structs are comparable only if all fields are comparable.
Slices, maps, and functions make structs non-comparable.
nil interface value has no dynamic type or value.
A typed nil pointer assigned to an interface is not equal to nil interface.
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.
Only when invariants are required (non-empty name, positive port).
Many stdlib types (bytes.Buffer, sync.Mutex) are designed for zero-value use.
Zero time.Time is January 1, year 1 UTC - rarely a useful sentinel.
Use pointers or time.Time.IsZero() checks.
type Outer struct{ Inner Inner }
o := Outer{Inner: Inner{Field: 1}}Unmentioned nested fields remain zero inside Inner.
Assignment copies the header; backing store is shared.
Initialize once and pass carefully to avoid unintended sharing.
Go zeros allocations for safety in many paths, but do not rely on that for crypto secrets.
Overwrite sensitive buffers explicitly when needed.
var vs := at declaration timeStack 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).
Reviewed by Chris St. John·Last updated Jul 19, 2026