Before you begin
Why this matters
Go never leaves a variable without a value, so the question is never “is it set?” but “is its zero value what I want?”. When you know the zero value of each type and what := does with each name, you can predict a crash on the first write to a map, and a variable that never seems to change.
What this helps you reason about
- Say what any variable or struct field holds before the code gives it a value.
- Tell a zero value that is ready to use, such as an empty list, from one that stops the program.
- Read a := with several names, and say which names are new and which are assigned.
In Java, a local variable that has no value yet cannot be read: the compiler refuses. In C, its value is indeterminate: often whatever was in that memory before. Go does neither. Every variable starts with a value chosen by its type, called its zero value. This lesson follows the zero value of each type, shows which ones you can use at once and which ones stop the program, and then looks at :=, which sometimes declares a new variable and sometimes assigns one that already exists.
Already know this? The short version
- Every variable starts at its type’s zero value: 0, false, "", nil for pointers, slices, maps, channels, functions, and interfaces, and for a struct, the zero value of each field.
- A nil slice works with len, range, and append, and strings.Builder, bytes.Buffer, and sync.Mutex work from their zero values. Writing to a nil map panics (assignment to entry in nil map), and so does reading through a nil pointer.
- A lookup of a missing key gives the zero value with no error; v, ok := m[k] tells a missing key from a stored zero, and t.IsZero() tells an unset time.Time.
- var gives a zero value and works everywhere; := works only inside functions, needs at least one new name on its left, and assigns the others when they were declared in the same block. In an inner block, every name on its left is new.
- The section “What goes wrong with zero values in real programs?” covers a map field that is never made, 0 as a real ID, JSON null against [], and := hiding err in an inner block.
Words in this lesson (8)
- zero value
- The value a variable holds when it is declared without one: 0 for numbers, false for bool, "" for string, and nil for pointers, slices, and maps.
- nil
- The zero value of pointers, slices, maps, channels, functions, and interfaces such as error. It means the value refers to nothing yet.
- struct
- A value that groups named fields, such as Card{Total: 5}. Assigning or passing a struct copies every field.
- map
- A built-in collection that stores values under keys, such as map[string]int. A map value refers to entries stored elsewhere, so a copy of it reaches the same entries.
- panic
- Go’s way of stopping a program because of a mistake in the code. panic("message") stops the current function and every function that called it, running their deferred calls on the way. Unless a deferred call recovers, the program prints panic: message and a stack trace, then exits.
- comma ok
- The two-result form value, ok := m[key]. ok is false when the key is missing, which the value alone cannot show.
- short variable declaration
- The form name := value, which declares a new variable in the current block. With several names on the left, at least one must be new in that block; the others, if already declared in that same block, are assigned.
- shadowing
- Declaring a variable in an inner block with the same name as an outer one. The inner variable hides the outer one until the block ends.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Zero values
What zero value does each type get?
Variables showed that a Go variable declared without a value holds the zero value of its type. This section gives the zero value of every kind of type.
Numbers start at 0, a bool at false, and a string at "", the empty string.
Six kinds of type start as nil, which means the value refers to nothing yet: pointers, slices, maps, channels, functions, and interfaces such as error. A channel, written chan, passes values between parts of a program that run at the same time; this lesson needs only its zero value.
A struct is a value that groups named fields. Its zero value has every field at that field’s own zero value. A struct literal, such as Toolbox{Owner: "Ada"}, sets the fields it names, and every other field gets its zero value.
type Toolbox struct {
Owner string
Nails int
Tools []string
Spare *Toolbox
}
var nails int // 0
var locked bool // false
var err error // nil
var box Toolbox
// box is {Owner: "", Nails: 0, Tools: nil, Spare: nil} Every variable starts with its type’s zero value: 0, false, "", nil, or a struct whose fields hold their zero values.
Coming from C: A C local int declared without a value has an indeterminate value; a Go var nails int is always 0.
Precisely: what the specification promises
The Go specification says that every variable is given its zero value when storage is allocated for it and no explicit value is given: by var, by new, by make, and by a composite literal for the fields it leaves out. Elements of arrays and slices are zeroed in the same way. new(Toolbox) returns a pointer to a new zero Toolbox. An array’s zero value is an array of zero elements, not nil.
Experiment
List the zero value of each type
Go sets every new variable to the zero value of its type before any code can read it. A struct’s zero value is made of the zero values of its fields, and a struct literal sets only the fields it names.
Why it matters You never need to ask whether a Go variable holds leftover data. Ask instead whether its zero value is the value you want.
Predict the three output lines. Then change var box Toolbox to box := Toolbox{Owner: "Ada"} and predict the last line again.
package main
import "fmt"
type Toolbox struct {
Owner string
Nails int
Tools []string
Spare *Toolbox
}
func main() {
var nails int
var owner string
var locked bool
var hooks []string
var err error
var box Toolbox
fmt.Println(nails, owner == "", locked)
fmt.Println(hooks == nil, err == nil)
fmt.Printf("%+v\n", box)
}
What to look for
Each variable appears in main’s frame after the stepper passes its declaration, already holding its zero value: nails 0, owner "", locked false, hooks []string len: 0, cap: 0, nil, err error nil, and box main.Toolbox {Owner: "", Nails: 0, Tools: []string len: 0, cap: 0, nil, Spare: nil}. The run prints 0 true false, then true true, then {Owner: Nails:0 Tools:[] Spare:<nil>}. %+v prints a struct with its field names. It prints the empty Owner as nothing and the nil slice as [], so the printed line hides what the frame shows. After the edit, box shows Owner "Ada" and every other field at its zero value, and the last line is {Owner:Ada Nails:0 Tools:[] Spare:<nil>}.
Expected output
0 true false
true true
{Owner: Nails:0 Tools:[] Spare:<nil>}02 / Ready to use
Which zero values are ready to use?
A zero value is useful when code can use it at once, with no setup. Go’s standard library is designed this way, and so is the nil slice.
A nil slice has length 0. len returns 0, a for range loop over it runs zero times, and append gives it new storage and returns a slice that holds the new elements. So var tools []string followed by append is the usual way to build a list. Only reading an element, such as tools[0], fails, because there is none.
Several standard types work the same way. A method is a function called on a value with a dot, such as label.WriteString("saw"). A zero strings.Builder holds no text, and its WriteString method adds text to it. A zero bytes.Buffer holds no bytes. A zero sync.Mutex, a lock for code that runs at the same time, is unlocked. You declare one with var and call its methods.
var tools []string
fmt.Println(len(tools)) // 0
tools = append(tools, "saw")
var label strings.Builder
label.WriteString("box of ")
label.WriteString(tools[0])
fmt.Println(label.String()) // box of saw A nil slice works as an empty list, and many standard types work from their zero value with no setup.
Precisely: designing for a useful zero value
Effective Go recommends types whose zero value is ready to use, because then var x T and a struct literal that leaves the field out both give a working value. A struct whose fields all have useful zero values is useful too. A strings.Builder or sync.Mutex must not be copied after its first use: a copied Builder panics on the next write, and go vet’s copylocks check reports a copied Mutex.
Experiment
Append to a nil slice
A nil slice has length 0, so range has nothing to visit and append makes new storage for the elements. A zero strings.Builder holds no text and is ready for WriteString. Reading tools[0] needs an element at index 0, and there is none.
Why it matters Declare a list with var and append to it; you do not need to make it first. Check len before you read an element.
Predict whether the line inside the for loop runs, and what the last line prints. Then add fmt.Println(tools[0]) on a new line just before tools = append(…) and predict again.
package main
import (
"fmt"
"strings"
)
func main() {
var tools []string
for _, tool := range tools {
fmt.Println("never printed:", tool)
}
tools = append(tools, "saw", "drill")
var label strings.Builder
label.WriteString("box of ")
label.WriteString(tools[1])
fmt.Println(len(tools), tools == nil, label.String())
}
What to look for
At the for line, tools shows []string len: 0, cap: 0, nil. The next stop is the append line: the loop body never runs. After append, tools shows len: 2, cap: 2 with "saw" and "drill", and nil is gone. label shows strings.Builder "" when it is declared, then "box of ", then "box of drill". The run prints 2 false box of drill. After the edit, the run stops at the new line with the error runtime error: index out of range [0] with length 0, and nothing is printed.
Expected output
2 false box of drill03 / Panics
Which zero values stop the program?
Two zero values stop the program when they are used the wrong way. Each stop is a panic: a run-time error that ends the program with a message.
A nil map can be read. Reading any key gives the zero value of the map’s values, and len gives 0. Writing to a nil map panics with the message panic: assignment to entry in nil map. So a map must be created before its first write: make(map[string]int) or the literal map[string]int{} each gives an empty map that is not nil. tools["saw"]++ adds 1 to the entry for "saw"; a missing entry counts from 0.
A nil pointer can be compared and copied, but reading or writing through it panics: panic: runtime error: invalid memory address or nil pointer dereference. A struct field that is a pointer starts as nil, so box.Spare.Owner panics until box.Spare points to a Toolbox.
var tools map[string]int
fmt.Println(tools["saw"]) // 0
tools = make(map[string]int)
tools["saw"]++
box := Toolbox{Owner: "Ada"}
// box.Spare is nil, so this line panics
fmt.Println(box.Spare.Owner) A nil map can be read but not written; a nil pointer cannot be read through.
Coming from Java: A Java Map field starts as null, and even get throws a NullPointerException. A Go map field starts as nil: reads work and give the zero value, and only the first write panics, often far from the line that forgot to make the map.
Precisely: the other nil values
delete on a nil map does nothing, and a for range loop over it runs zero times. Calling a nil function panics. Sending to or receiving from a nil channel waits forever; when nothing else can run, the Go runtime stops the program with a fatal error that reports a deadlock.
Experiment
Write to a nil map
A nil map has no storage for entries. A read can still answer with the zero value, but a write needs a place to put the new entry, so Go panics. make creates that storage.
Why it matters Every map that will be written to must be made first, with make or a literal such as map[string]int{}.
Predict both output lines. Then delete the line tools = make(map[string]int) and predict what happens at tools["saw"]++.
package main
import "fmt"
func main() {
var tools map[string]int
fmt.Println(tools["saw"], len(tools))
tools = make(map[string]int)
tools["saw"]++
fmt.Println(tools["saw"], len(tools))
}
What to look for
At the first Println, tools shows map[string]int nil, and the line prints 0 0: reading a nil map works. After make, tools shows map[string]int [], an empty map that is not nil. After tools["saw"]++, it shows ["saw": 1], and the run prints 1 1. After the edit, tools stays nil, the first line still prints 0 0, and the run stops at tools["saw"]++ with the error assignment to entry in nil map. The stepper shows the message without the word panic; a program run outside the stepper prints panic: assignment to entry in nil map.
Expected output
0 0
1 104 / Missing or zero
When does a zero value look like a real value?
A zero value is a real value of its type. So when code receives 0 or "", it cannot tell whether someone chose that value or nothing was ever set.
A map lookup shows the problem. Reading a key that is not in the map gives the zero value, with no error. Below, the saw hangs on hook 0, the first hook, and the drill is not in the map at all. Both lookups give 0.
The comma ok form tells the two apart. hook, ok := hooks["drill"] gives the value and a bool, ok, which is true when the key is in the map and false when it is not.
Time has the same problem. time.Time is the standard type for a moment in time. Its zero value is a real date, January 1 of year 1, which prints as 0001-01-01 00:00:00 +0000 UTC. Ask whether a time was ever set with its IsZero method, not by looking at the date.
hooks := map[string]int{"saw": 0, "hammer": 1}
fmt.Println(hooks["saw"], hooks["drill"]) // 0 0
hook, ok := hooks["drill"]
fmt.Println(hook, ok) // 0 false
var borrowedOn time.Time
fmt.Println(borrowedOn.IsZero()) // true A zero value can also be a real value; use comma ok for a map, and IsZero for a time.Time.
Coming from Python: hooks["drill"] raises a KeyError in Python; in Go it gives 0 and the program goes on. Go’s hook, ok := hooks["drill"] does the job of "drill" in hooks.
Precisely: other ways to mark “not set”
A struct field can be a pointer, such as *int, so that nil means “not set” and a pointer to 0 means a real 0. Two other forms give a comma ok result: a type assertion, v, ok := x.(T), and a receive from a channel, v, ok := <-ch.
Experiment
Mistake a missing tool for hook 0
A lookup of a key that is not in the map gives the zero value of the map’s values, here 0, with no error. 0 is also a real hook. The second result of the comma ok form says whether the key was there.
Why it matters When the zero value can also be a real value, read a map with v, ok := m[key] and check ok.
The saw hangs on hook 0, and there is no drill. Predict both output lines. Then replace hook := hooks[tool] with hook, ok := hooks[tool], add the line if !ok { return tool + " is missing" } right after it, and predict again.
package main
import "fmt"
func whereIs(hooks map[string]int, tool string) string {
hook := hooks[tool]
return fmt.Sprint(tool, " on hook ", hook)
}
func main() {
hooks := map[string]int{"saw": 0, "hammer": 1}
fmt.Println(whereIs(hooks, "saw"))
fmt.Println(whereIs(hooks, "drill"))
}
What to look for
Step into whereIs twice. For "saw" and for "drill", hook shows 0 at the return line, and the run prints saw on hook 0, then drill on hook 0: nothing in the frame or the output tells the two apart. After the edit, the frame also lists ok: true for the saw and false for the drill. The second call returns at the if, and the run prints saw on hook 0, then drill is missing.
Expected output
saw on hook 0
drill on hook 005 / var and :=
When does := declare a new variable?
var and := both declare variables. var name Type declares a variable that holds its zero value; var name = value gives it a value. name := value declares a variable and gives it a value in one step, with the type taken from the value.
:= works only inside a function. At package level, outside every function, nails := 40 does not compile: syntax error: non-declaration statement outside function body. Write var nails = 40 there.
:= can have several names on its left, and at least one of them must be new in the current block. A name already declared in that same block is not declared again: := assigns it. The Go specification calls this redeclaration. Below, the second := declares drills and assigns found, so main has one found, which changes from true to false. If no name on the left is new, the compiler refuses: no new variables on left side of :=.
Redeclaration happens only in the same block. In an inner block, such as an if body or a loop body, every name on the left of := is new there, so := declares a second found that hides the first. Scope and shadowing shows that bug with err.
saws, found := count("saw")
// drills is new; found is assigned
drills, found := count("drill")
fmt.Println(saws, drills, found) // 2 0 false Use var for a zero value and := for a value. A := assigns the names already declared in the same block and declares the rest.
Coming from JavaScript: let found twice in one block is an error in JavaScript and TypeScript. Go’s := accepts a name declared in the same block, and assigns it, as long as another name on the left is new.
Precisely: choosing between var and :=
Go programmers often write var total int when the zero value is the starting point, and total := 10 when it is not, so the reader sees which was meant. var is also how to choose a type other than the value’s default, as in var ratio float64 = 3. A redeclared variable keeps its type, so the new value must have that type: found cannot become a string.
Experiment
Assign an old name with :=
In one block, := declares the names on its left that are new and assigns the ones already declared in that block. drills is new, so the line is allowed, and found is assigned. With saws in place of drills, no name is new, and the compiler refuses the line.
Why it matters In a := with several names, check which names are already declared in the same block: those are assigned, not declared.
Predict what the run prints, and how many variables named found main’s frame lists. Then change drills to saws on the line drills, found := count("drill") and predict again.
package main
import "fmt"
func count(tool string) (int, bool) {
if tool == "saw" {
return 2, true
}
return 0, false
}
func main() {
saws, found := count("saw")
drills, found := count("drill")
fmt.Println(saws, drills, found)
}
What to look for
After the first :=, the frame lists saws 2 and found true. After the second, it lists saws, found, and drills, with one found, now false: the second := assigned it. The run prints 2 0 false. After the edit, the program does not compile. The first error is no new variables on left side of := at that line; a second one, undefined: drills, points at the Println.
Expected output
2 0 false06 / In production
What goes wrong with zero values in real programs?
Each of these bugs compiles.
A map field that is never made. A struct literal that leaves out a map field, or var shed Shed, gives a nil map, and the first write panics, often in another function. Make the map in a constructor function, a function that creates and returns the value, as below, or make it just before the first write when the field is nil.
A zero that looks like a real ID. A struct that was never filled in has ID 0. If 0 is also a real ID, “not found” and “the first record” look the same. Start IDs at 1, or return a found bool or an error with the struct.
JSON null against []. json.Marshal, from encoding/json, turns a value into JSON text; the text in backquotes after a struct field, such as `json:"borrowed"`, is a tag that gives the field’s name in the JSON. json.Marshal writes a nil slice as null and an empty one as []. A phone app or a web page that reads the list may fail on null. When a field must always be a list, set it to []string{} before encoding.
A := that hides err. data, err := load() followed by extra, err := parse(data) assigns err, because extra is new. Move the second line into an if body and both names are new there, so the outer err never receives the error. Go error values, wrapping, and errors.Is shows where such an error goes.
type Shed struct {
Name string
Tools map[string]int
}
func NewShed(name string) Shed {
return Shed{Name: name, Tools: map[string]int{}}
} Make map fields when the struct is created, mark “not set” separately from 0, give JSON list fields a non-nil slice, and check every := inside an inner block.
Precisely: omitempty
Adding ,omitempty to a tag, as in `json:"borrowed,omitempty"`, leaves the field out of the JSON when it is nil or empty. That is a third form, different from both null and [], so the program that reads the JSON must expect it.
Experiment
Send a nil slice as JSON
encoding/json writes a nil slice as null and an empty slice that is not nil as []. Both have length 0, so only the JSON and the frame show which one the field held.
Why it matters When other programs read your JSON, give every list field a non-nil slice, such as []string{}, before you encode it.
No neighbour has borrowed a tool from Ada. Predict both output lines. Then change var borrowed []string to borrowed := []string{} and predict again.
package main
import (
"encoding/json"
"fmt"
)
type Toolbox struct {
Owner string `json:"owner"`
Borrowed []string `json:"borrowed"`
}
func main() {
var borrowed []string
box := Toolbox{Owner: "Ada", Borrowed: borrowed}
data, _ := json.Marshal(box)
fmt.Println(string(data))
fmt.Println(box.Borrowed)
}
What to look for
borrowed shows []string len: 0, cap: 0, nil, and box shows the same nil slice in Borrowed. data holds the JSON text as bytes, so the stepper shows it as []uint8 with numbers, one per character (123 is the code of {); read the output instead. The run prints {"owner":"Ada","borrowed":null}, then []. fmt prints the nil slice as [], so the second line hides the difference. After the edit, borrowed shows len: 0, cap: 0, [] without nil, and the first line becomes {"owner":"Ada","borrowed":[]}.
Expected output
{"owner":"Ada","borrowed":null}
[]07 / Try it
Can you predict what each variable holds?
Decide what each zero value gives, then which names the second := declares.
package main
import "fmt"
type Toolbox struct {
Owner string
Tools []string
Hooks map[string]int
}
func main() {
var box Toolbox
box.Tools = append(box.Tools, "saw")
hook, ok := box.Hooks["saw"]
owner, hook := box.Owner+"?", hook+len(box.Tools)
fmt.Println(len(box.Tools), hook, ok, owner)
}
What is printed?
Reveal the reasoning
1 1 false ?. box starts with every field at its zero value. append works on the nil Tools, so Tools holds one tool. Hooks is a nil map, and reading it is allowed: the lookup gives 0 and ok false. The second := declares owner, which is new, and assigns hook, which was declared in the same block, so hook becomes 0 + 1. Owner is "", so owner is "?".
Practice tickets
- GO-301 · The recycling count stops at the first item → Free
- GO-302 · A child set to no play time still gets 30 minutes → Sign in · Pro
- GO-303 · Rooms without sensors break the alarm app → Sign in · Pro
Keep exploring
Go slice headers, append, and capacity →
Primary references
- The Go Programming Language Specification: The zero value ↗
- The Go Programming Language Specification: Short variable declarations ↗
- The Go Programming Language Specification: Index expressions ↗
- Effective Go: Allocation with new ↗
- Package encoding/json: Marshal ↗
- Package time: Time.IsZero ↗
- Package strings: Builder ↗