Zero Values and Initialization

Updated

September 13, 2026

Zero Values and Initialization

Every variable in Go has a value from the moment it exists. The boring default is: declare with var when the zero is what you want, assign with := when you have a real value, call make only for slices, maps, and channels you are about to fill.

Mental model

The zero value is the language’s “empty but usable” bit pattern for a type: 0, "", false, nil. You never read uninitialised memory. That is why a missed assignment often looks like a quiet 0 instead of a crash — and why zeros are part of the design, not an accident.

How What you get
var x T x is type T, value is zero
x := v type of x is the type of v
new(T) *T pointing at a zero T
make(slice/map/chan) a live header, not nil (for the length you asked)

new is rarely needed: &Ticket{} is the same idea and names the fields. make is not optional for maps you will write.

Worked examples

Case 1: Zeros you can print

Save as zeros.go. One var per kind, no assignment.

// zeros.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func main() {
    var open bool
    var tables int
    var name string
    var ticket *Ticket
    var items []string
    var prices map[string]int
    var t Ticket

    fmt.Printf("bool %v\n", open)
    fmt.Printf("int %d\n", tables)
    fmt.Printf("string %q\n", name)
    fmt.Printf("pointer nil=%v\n", ticket == nil)
    fmt.Printf("slice nil=%v len=%d\n", items == nil, len(items))
    fmt.Printf("map nil=%v len=%d\n", prices == nil, len(prices))
    fmt.Printf("struct %+v\n", t)
}

Run:

go run zeros.go

Output:

bool false
int 0
string ""
pointer nil=true
slice nil=true len=0
map nil=true len=0
struct {ID:0 Table:0}

A nil slice has length 0. A nil map has length 0. They are not the same as empty-but-allocated, which is what make gives you.

Case 2: := when you already know the value

Save as short_decl.go. Short declaration infers the type from the right-hand side and must introduce at least one new name.

// short_decl.go
package main

import "fmt"

func main() {
    name := "Amina"
    tables := 3
    open := true
    fmt.Printf("%s %d open=%v\n", name, tables, open)
}

Run:

go run short_decl.go

Output:

Amina 3 open=true

Use var tables int = 3 when you want the type on the left for a reader. Use := when the type is obvious from the value.

Case 3: make for a map and a slice you will fill

Save as make_desk.go. append on a nil slice is fine. A write to a nil map panics (next chapter on maps shows that crash). make the map before the first write.

// make_desk.go
package main

import "fmt"

func main() {
    var items []string
    items = append(items, "toast")
    items = append(items, "tea")

    prices := make(map[string]int)
    prices["toast"] = 350
    prices["tea"] = 250

    fmt.Println(items)
    fmt.Println(prices["toast"], prices["tea"])
}

Run:

go run make_desk.go

Output:

[toast tea]
350 250

make([]string, 0, 8) is the same story with a hint: length 0, capacity 8, so the first few appends do not allocate again.

Case 4: new vs a pointer to a composite literal

Save as new_ticket.go. new(Ticket) returns a pointer to a zero ticket. &Ticket{ID: 7} does the same job and lets you set fields.

// new_ticket.go
package main

import "fmt"

type Ticket struct {
    ID    int
    Table int
}

func main() {
    a := new(Ticket)
    b := &Ticket{}
    c := &Ticket{ID: 7, Table: 12}
    fmt.Printf("new  %#v nil=%v\n", a, a == nil)
    fmt.Printf("&{}  %#v\n", b)
    fmt.Printf("lit  %#v\n", c)
}

Run:

go run new_ticket.go

Output:

new  &main.Ticket{ID:0, Table:0} nil=false
&{}  &main.Ticket{ID:0, Table:0}
lit  &main.Ticket{ID:7, Table:12}

a is not nil. It points at zeros. Prefer c when you have data; prefer var t Ticket when a value (not a pointer) is enough.

The trap

Zeros look like data. A function that returns int cannot tell “table 0” from “I forgot to set the table” unless you add an error or a separate ok flag. Save as zero_table.go:

// zero_table.go
package main

import "fmt"

func tableFor(name string) int {
    if name == "window" {
        return 4
    }
    return 0
}

func main() {
    fmt.Println("window", tableFor("window"))
    fmt.Println("missing", tableFor("patio"))
}

Run:

go run zero_table.go

Output:

window 4
missing 0

The second line is not a table. It is the zero. The fix is the comma-ok style you will use on maps, or an error: func tableFor(name string) (int, error). Do not invent a sentinel like -1 unless the domain already uses it.

The boring rule

  • var x T when the zero is the right start (counters, flags, buffers you append to).
  • x := v when v is the real value.
  • make maps before write. make slices when you know length or capacity.
  • new(T) and &T{} both yield a non-nil *T. Prefer the literal.
  • Treat 0, "", and nil as values you will see in production, not as “unset” unless you also return error.
  • Nil slice: append is safe. Nil map: write is not.

Try this

  1. In zeros.go, append one string onto items and print items == nil again. After append it is no longer nil.
  2. In make_desk.go, replace make(map[string]int) with var prices map[string]int and run. Read the panic.
  3. In new_ticket.go, add var t Ticket and p := &t. Print p. Same shape as new, different way to get there.
  4. Change tableFor to return (int, bool) and make main print "none" when the bool is false.