4.3 Zero Values: The Default Initial State of Every Type
Right, let’s talk about what happens when you declare a variable but don’t give it a value. In most languages, this leaves you with a landmine—an undefined value that’ll blow up your program the moment you look at it funny. Go takes a radically different, and frankly, more sensible, approach: it gives every single type a pre-packaged, ready-to-go default value. This is its zero value.
Think of it as Go making your bed for you. You might want to mess with the pillows later, but you’re not going to fall into a tangled heap of sheets and regret the moment you were born. This design choice eliminates whole categories of bugs related to uninitialized variables and makes code predictably safe. The compiler ensures that every variable always holds some valid value, even if it’s just the placeholder.
The Zero Value Roster
Here’s what you get, by type, when you declare a variable with var:
- Numeric types (
int,float32,complex64, etc.): A solid, dependable0. Notundefined, notnull, just0. It’s the mathematical foundation upon which you can build. bool:false. The default state is off. Make of that what you will.string:""(an empty string). It’s not a null pointer; it’s a perfectly valid string you can query for length or append to without causing a panic.- Pointers, slices, maps, channels, functions, and interfaces:
nil. This is their zero value. It’s a designated “nothing” value that you can check for, which is infinitely better than a segfault.
var (
i int // 0
f float64 // 0
b bool // false
s string // ""
p *int // nil
sl []int // nil
m map[string]int // nil
c chan int // nil
f func() // nil
iface interface{} // nil
)
fmt.Printf("int: %d, float64: %f, bool: %t, string: '%s'\n", i, f, b, s)
fmt.Printf("pointer is nil: %t\n", p == nil)
// Output: int: 0, float64: 0.000000, bool: false, string: ''
// Output: pointer is nil: true
Why This Is a Brilliant Design Choice
This isn’t an accident; it’s a cornerstone of Go’s philosophy. It provides memory safety and immediate usability. The runtime always initializes the memory to something sane. You can create a struct and start using it immediately without writing a dozen constructors first. It makes structs truly usable without explicit initialization.
type Config struct {
Port int
Enabled bool
Name string
}
func main() {
var cfg Config
// This is perfectly safe. No null reference exceptions here.
fmt.Printf("Starting server on port %d...\n", cfg.Port) // Output: Starting server on port 0...
}
Okay, so maybe you don’t want your server running on port 0, but it didn’t crash! This leads us to the most important pitfall.
The Most Common Pitfall: Assuming Meaning
The biggest mistake is assuming the zero value is meaningful for your application’s logic. A 0 for a Port field is technically valid but semantically nonsense. An empty string for a DatabaseURL is a valid string but will fail spectacularly when you try to connect.
This is why you must differentiate between a technical zero value and a logical default. The zero value gives you safety; you provide the meaning. This is often done through constructor functions or explicit initialization after declaration.
// Bad: Your code might have to check for port == 0 everywhere.
func (c *Config) Validate() error {
if c.Port == 0 {
return errors.New("port is required")
}
return nil
}
// Better: Use a constructor function to establish sensible defaults.
func NewConfig() *Config {
return &Config{
Port: 8080, // A logical default, not the technical zero value
Enabled: true,
}
}
The nil Slice and Map: Your Best Friends
This one trips up newcomers from other languages. A nil slice or map is perfectly valid and often preferable to an empty one.
- A
nilslice has a length and capacity of 0. You canappendto it, which will allocate a new underlying array, and you can range over it (which will do nothing, gracefully). - A
nilmap cannot have keys inserted into it. Trying to do so will cause a panic. You must initialize it withmakeor a composite literal.
The beauty is that you can write functions that accept slices without worrying if they’re nil or not.
var names []string // nil slice
fmt.Println(len(names)) // 0
fmt.Println(names == nil) // true
// This is perfectly safe and idiomatic.
for i, name := range names {
fmt.Println(i, name)
} // No output, no panic.
// Append works on nil slices.
names = append(names, "Alice")
fmt.Println(names) // [Alice]
var scores map[string]int // nil map
// scores["alice"] = 100 // PANIC: assignment to entry in nil map
// So you must initialize a map before use.
scores = make(map[string]int)
// or
// scores = map[string]int{}
The zero value isn’t a quirky language feature; it’s a deliberate tool for writing simpler, more robust code. Embrace it. Use it to your advantage. And always, always remember that your program’s semantics are your responsibility, not the compiler’s.