1. Introduction
When you’re just starting to program, it can feel like “declared a variable” and “assigned a value to it” are the same action, just sometimes written on two lines. In practice, they’re different things: a declaration creates a variable (name + type), and an assignment changes its contents. In Go, there’s never an “empty gap” between these steps: as soon as a variable is created, it automatically gets the default value for its type.
That value is called the zero value. It’s not “zero” in the sense of “always 0,” but “zero” in the sense of “the most basic value of the type that is safe to use.” For numbers it’s zero, for bool it’s false, and for strings it’s the empty string. The main practical benefit: you can immediately read a variable after declaration—without the risk of pulling random garbage and getting “works today, breaks tomorrow” behavior.
2. Zero values of basic types
If you don’t lock zero values into your head like a table, they turn into “magic knowledge,” and magic in programming usually ends the same way: someone crying over the debugger. So let’s explicitly look once at which default values the basic types we’re working with have.
| Type in Go | Zero value | What it usually means in code | Where there can be a trap |
|---|---|---|---|
|
|
“nothing accumulated,” “counter is empty” | 0 can be real input, not “not set” |
|
|
“no sum,” “still zero” | 0.0 can be a valid value (temperature, balance) |
|
|
“flag is off,” “condition not met” | false can mean both “no” and “not checked yet” |
|
|
“empty text,” “nothing set” | an empty string is sometimes acceptable as a real value |
Now a small demo: declare variables and print them immediately. The output helps your brain cement the fact.
package main
import "fmt"
func main() {
var n int
var f float64
var ok bool
var s string
fmt.Println(n) // 0
fmt.Println(f) // 0
fmt.Println(ok) // false
fmt.Println(s) // (empty string, visually "nothing")
}
Pay attention to the string: the empty string prints as “nothing,” and that can be confusing sometimes. You can do a simple trick: put markers around the string.
package main
import "fmt"
func main() {
var name string
fmt.Println(">", name, "<") // > <
}
int: counters, sums, and why 0 is a great starting point
With integers, the zero value is especially pleasant, because a huge chunk of programs are counters, sums, and other accumulators. We constantly write “add up N numbers,” “count how many elements match,” “find the maximum.” And almost always, the natural starting point is zero. Go makes it so that even if you forgot to “initialize the sum to zero,” it will still be zero (though it’s better not to turn this into a habit).
Let’s look at a mini snippet that seems boring but is actually the foundation of half the loop-based algorithms: accumulating a sum.
package main
import "fmt"
func main() {
var sum int // zero value = 0
sum = sum + 5
sum = sum + 7
fmt.Println(sum) // 12
}
The key idea here: sum was already a valid number at the moment of declaration. We weren’t afraid to add to it because “0 + 5” is normal arithmetic.
Now let’s take a small step toward reality: compute the sum of several entered numbers (we already know for and fmt.Scan).
package main
import "fmt"
func main() {
var sum int
for i := 0; i < 3; i++ {
var x int
fmt.Scan(&x)
sum = sum + x
}
fmt.Println(sum) // sum of three numbers
}
Here sum is a classic example of a variable that perfectly legitimately lives at its zero value and waits until you start accumulating something.
bool: flags, conditions, and the “nothing found” state
Boolean variables are a favorite tool when we need to record a fact: “did we find something,” “did we meet the condition at least once,” “is everything OK.” And almost always it’s convenient that the flag is off by default (false). That’s logical: until we’ve done anything, we haven’t found anything and haven’t confirmed anything.
Imagine a task: we read several numbers and want to know whether the number 10 appeared among them. The found flag can be declared with var without initialization: it will be false anyway.
package main
import "fmt"
func main() {
var found bool // zero value = false
for i := 0; i < 3; i++ {
var x int
fmt.Scan(&x)
if x == 10 {
found = true
}
}
fmt.Println(found) // true if the numbers included 10
}
In effect, the zero value gives us an honest state of “there hasn’t been a reason to say true yet.” That’s convenient and readable.
But this is also where a subtle point appears: false can mean both “didn’t find it” and “didn’t even try to look.” In simple tasks it doesn’t matter, but in larger programs you sometimes have to separate these meanings.
string: the empty string as a normal state
With strings, beginners often fall into a psychological trap: it feels like a string “must be filled,” otherwise it’s an error. In reality, the empty string is a perfectly normal value. Moreover, it’s very useful: you can safely concatenate strings, you can check whether “the string is empty or not,” and you can store text that will be provided later.
Let’s look at the simplest example: declare a string and check whether it’s empty.
package main
import "fmt"
func main() {
var title string // zero value = ""
fmt.Println(title == "") // true
}
The empty string is also convenient because it’s usually easy to check. If you need to distinguish “the user entered nothing” from “the user entered some name,” then "" is a natural candidate for the “still empty” state.
One more simple but important point: concatenation with an empty string is safe.
package main
import "fmt"
func main() {
var prefix string // ""
result := prefix + "Go"
fmt.Println(result) // Go
}
We’re not building complex string pipelines yet, but the fact that “a string is always valid” makes life much easier: no null, no crashes because someone forgot to initialize a variable.
float64: zero with a decimal point and small traps
Floating-point numbers (float64) are numbers with a fractional part, and their zero value also looks like zero. When printed via fmt.Println, it’s usually displayed as 0 (without “.0”), but conceptually it’s 0.0. And it’s convenient for the same reasons as with int: sums, accumulation, comparisons with a threshold—all of that naturally starts at zero.
For example, we compute total study time over several days (let’s say hours can be fractional, like 1.5).
package main
import "fmt"
func main() {
var total float64
for i := 0; i < 2; i++ {
var hours float64
fmt.Scan(&hours)
total = total + hours
}
fmt.Println(total) // sum of hours
}
Here too you can see the advantage: total doesn’t need to be “prepared”—it’s already valid.
But with float64 there’s a particularly unpleasant area you should remember even at a basic level: division by zero. Even if you’re not diving deep yet into how exactly floating-point numbers behave, the practical rule is simple: if you’re going to divide by a value that could still be at its zero value (for example, a day counter), check that it’s not zero. That’s not “paranoia,” it’s reasonable programming.
3. When the zero value turns into a bug
The zero value is a gift, but like many gifts, it can come with a surprise. The main surprise: people often use the zero value as “a sign that there was no value,” but 0, "", and false can absolutely be real data. And then the program starts behaving logically incorrectly: it compiles, it runs, but it messes up.
A classic example is finding a maximum. A beginner (and sometimes not only a beginner) writes: “let the maximum initially be zero.” This is fine if all values are guaranteed to be non-negative. But if values can be negative (for example, winter temperature), the maximum will never update and will remain 0, even if there was no zero at all.
Here’s an example of “dangerous” code—it looks reasonable, but the logic depends on the input data:
package main
import "fmt"
func main() {
var max int // 0
for i := 0; i < 3; i++ {
var x int
fmt.Scan(&x)
if x > max {
max = x
}
}
fmt.Println(max) // may print 0 even if all numbers are negative
}
How do you live with this without introducing “magic numbers” and without jumping ahead into advanced topics? One simple technique is to add a separate flag that indicates the value has already been “initialized from data.”
package main
import "fmt"
func main() {
var max int
var hasMax bool // false: max has not been set from input yet
for i := 0; i < 3; i++ {
var x int
fmt.Scan(&x)
if !hasMax || x > max {
max = x
hasMax = true
}
}
fmt.Println(max) // correct even for negatives
}
Here, the zero value for hasMax works in our favor: false honestly means “there hasn’t been a single number yet.” Then we ourselves switch the flag to true when we get the first data.
A small interesting fact from the standard library world: the approach of “reset old values to the zero value” is used in real APIs as well. For example, some operations on slices (we’ll cover them later) deliberately “clear the tail” by setting elements to their zero value so they don’t keep extra references in memory. This isn’t just theory—it’s engineering practice.
Another interesting fact: in some serialization formats, “zero” fields can be omitted because when reading they will get the zero value anyway. For example, in the design discussion for gob, the idea is explicitly emphasized: “if you don’t set a value, it will be the zero value anyway, and you don’t need to transmit it.”
4. Mini program DailyStats
Now let’s put everything together into a small example that looks like a “real program” but still uses only constructs you already know: var, for, if, fmt.Scan, fmt.Println. The program will read the number of days and the daily number of hours (a float), compute the total sum, the maximum value, and a flag indicating “was there a day where the hours were at least the goal.”
First, a “simple and straightforward” version where we assume hours can’t be negative (reasonable for a study tracker).
package main
import "fmt"
func main() {
var days int
var goal float64
fmt.Scan(&days, &goal)
var total float64
var max float64
var reached bool
for i := 0; i < days; i++ {
var hours float64
fmt.Scan(&hours)
total = total + hours
if hours > max {
max = hours
}
if hours >= goal {
reached = true
}
}
fmt.Println(total) // for example: 7.5
fmt.Println(max) // for example: 3
fmt.Println(reached) // for example: true
}
Here total, max, reached start at their zero values (0, 0, false) and it feels natural. Until we’ve read any day, the sum is zero, the maximum is zero, and the goal hasn’t been reached.
But let’s make the program more robust and also illustrate the idea “don’t overuse the magic zero.” Add a hasMax flag so the maximum works correctly even if the data can be anything (maybe you reuse the code for temperatures, balances, or something else).
package main
import "fmt"
func main() {
var days int
var goal float64
fmt.Scan(&days, &goal)
var total float64
var max float64
var hasMax bool
var reached bool
for i := 0; i < days; i++ {
var hours float64
fmt.Scan(&hours)
total = total + hours
if !hasMax || hours > max {
max = hours
hasMax = true
}
if hours >= goal {
reached = true
}
}
fmt.Println(total)
fmt.Println(max)
fmt.Println(reached)
}
And here you can notice something nice: the zero value became part of the design. hasMax defaults to false, and that’s not a “language accident,” but a clear meaning: “the maximum hasn’t been defined by data yet.”
5. Typical mistakes when working with zero values
Mistake #1: thinking an “uninitialized variable” contains garbage.
In Go, that’s not the case: as soon as a variable is declared, it already holds a correct default value. Because of this, many Go programs are easier to read: you see fewer “ritual initializations” that other languages require only for safety.
Mistake #2: using 0, "", or false as a universal marker meaning “no value set.”
Sometimes it works; sometimes it breaks the logic so quietly that you’ll notice only from incorrect results. If zero (or the empty string) can be real data, it’s better to separate “a value exists” from “no value” with a separate bool flag, like we did with hasMax.
Mistake #3: accidentally relying on the zero value where a variable was supposed to get data from input.
This is a tricky situation: fmt.Scan might read nothing (invalid input, empty stdin), and the variable will remain at its zero value, and the program will keep computing as if the user entered zero. From the outside it looks like “the program works,” but in reality it silently accepts invalid data.
Mistake #4: incorrect initialization of a maximum or minimum due to the “convenient zero.”
If you’re searching for a maximum, initializing it to zero is only safe if you are sure the numbers are non-negative. Otherwise you’ll get max = 0 even when there isn’t a single zero in the input. In practice exercises this happens so often that it’s basically a genre: “why is my maximum always zero.”
Mistake #5: confusion caused by printing an empty string.
fmt.Println(s) for s == "" looks like nothing was printed, and a beginner decides the string “doesn’t exist.” The string exists—it’s just empty. If you need to see it with your own eyes, use markers like fmt.Println(">", s, "<") so the emptiness becomes visible.
GO TO FULL VERSION