DEV Community

Triệu Mẫn Trà
Triệu Mẫn Trà

Posted on AI-assisted

[Golang] Why Go Variables Have Zero Values Instead of Being Uninitialized

Consider this Go program:

package main

import "fmt"

func main() {
    var count int
    var enabled bool
    var name string

    fmt.Println(count)
    fmt.Println(enabled)
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

It prints:

0
false

Enter fullscreen mode Exit fullscreen mode

We never assigned anything to these variables.

So why do they already have values?

Because in Go, a variable is never in an uninitialized state. If you don't provide an initial value, Go gives it the type's zero value.

This sounds like a small language feature, but it has a surprisingly large effect on how Go programs are designed.

What Is a Zero Value?

Every Go type has a default value called its zero value.

Some common examples are:

int        → 0
float64    → 0
bool       → false
string     → ""
pointer    → nil
slice      → nil
map        → nil
channel    → nil
interface  → nil
function   → nil
Enter fullscreen mode Exit fullscreen mode

Structs are initialized recursively.

For example:

type User struct {
    Name   string
    Age    int
    Active bool
}
Enter fullscreen mode Exit fullscreen mode

Then:

var user User
Enter fullscreen mode Exit fullscreen mode

is effectively:

User{
    Name:   "",
    Age:    0,
    Active: false,
}
Enter fullscreen mode Exit fullscreen mode

You don't need to initialize every field manually.

Small Runnable Example

Create main.go:

package main

import "fmt"

type User struct {
    Name   string
    Age    int
    Active bool
    Tags   []string
}

func main() {
    var user User

    fmt.Printf("%+v\n", user)

    fmt.Println(user.Name == "")
    fmt.Println(user.Age == 0)
    fmt.Println(user.Active == false)
    fmt.Println(user.Tags == nil)
}
Enter fullscreen mode Exit fullscreen mode

Run:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Output:

{Name: Age:0 Active:false Tags:[]}
true
true
true
true
Enter fullscreen mode Exit fullscreen mode

Notice something interesting about Tags.

Its zero value is nil.

fmt displays a nil slice as [], but:

user.Tags == nil
Enter fullscreen mode Exit fullscreen mode

is still true.

What Actually Happens?

The important rule is simple:

Storage for a variable without an explicit initializer starts with the zero value of its type.

This behavior is guaranteed by the Go language.

Conceptually:

var n int
Enter fullscreen mode Exit fullscreen mode

starts as:

n = 0
Enter fullscreen mode Exit fullscreen mode

And:

var user User
Enter fullscreen mode Exit fullscreen mode

starts roughly like:

user.Name = ""
user.Age = 0
user.Active = false
user.Tags = nil
Enter fullscreen mode Exit fullscreen mode

The compiler and runtime do not need to literally generate those assignments one by one.

The exact implementation depends on where the variable lives and how the compiler optimizes the program.

What matters at the language level is that your Go code always observes the correct zero value.

Why Is This Useful?

One major benefit is deterministic state.

Imagine a language where:

var count int
Enter fullscreen mode Exit fullscreen mode

could contain whatever bytes happened to exist in memory previously.

Then:

if count > 100 {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

could behave unpredictably.

You would have to remember to initialize everything before using it.

Go removes that entire category of state.

A variable begins with a predictable value.

memory allocated
      |
      v
known zero state
      |
      v
program modifies it
Enter fullscreen mode Exit fullscreen mode

This simplifies reasoning about programs.

But Go takes the idea further.

Zero values often influence API design.

Zero Values Can Make Types Immediately Useful

Consider sync.Mutex:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex

    mu.Lock()
    fmt.Println("locked")
    mu.Unlock()
}
Enter fullscreen mode Exit fullscreen mode

There is no constructor:

mu := NewMutex()
Enter fullscreen mode Exit fullscreen mode

The zero value of sync.Mutex is already ready to use.

The same idea appears in many standard-library types.

For example:

var buf bytes.Buffer
Enter fullscreen mode Exit fullscreen mode

You can immediately do:

buf.WriteString("hello")
Enter fullscreen mode Exit fullscreen mode

without calling something like:

buf := bytes.NewBuffer(...)
Enter fullscreen mode Exit fullscreen mode

This leads to a useful Go design principle:

When practical, make the zero value of your type useful.

Designing Your Own Useful Zero Value

Suppose we create a counter:

type Counter struct {
    value int
}

func (c *Counter) Increment() {
    c.value++
}

func (c *Counter) Value() int {
    return c.value
}
Enter fullscreen mode Exit fullscreen mode

Now this works immediately:

var counter Counter

counter.Increment()
counter.Increment()

fmt.Println(counter.Value())
Enter fullscreen mode Exit fullscreen mode

Output:

2
Enter fullscreen mode Exit fullscreen mode

We didn't need:

counter := NewCounter()
Enter fullscreen mode Exit fullscreen mode

Because value naturally starting at 0 gives the type a meaningful initial state.

This makes APIs simpler.

Instead of:

counter := NewCounter(0)
Enter fullscreen mode Exit fullscreen mode

users can simply write:

var counter Counter
Enter fullscreen mode Exit fullscreen mode

Common Mistake: Zero Value Does Not Mean "Ready to Use"

A common misunderstanding is:

If every type has a zero value, every zero value must be usable.

That is not true.

Consider a map:

var users map[string]string

fmt.Println(users["alice"])
Enter fullscreen mode Exit fullscreen mode

Reading from the nil map is valid.

But this:

users["alice"] = "Alice"
Enter fullscreen mode Exit fullscreen mode

panics:

assignment to entry in nil map
Enter fullscreen mode Exit fullscreen mode

You need to initialize it first:

users := make(map[string]string)
users["alice"] = "Alice"
Enter fullscreen mode Exit fullscreen mode

Channels have another interesting zero-value behavior:

var ch chan int
Enter fullscreen mode Exit fullscreen mode

Here:

ch == nil
Enter fullscreen mode Exit fullscreen mode

is true.

Sending to or receiving from a nil channel does not panic.

Instead, it blocks forever.

Pointers also have a nil zero value:

var user *User
Enter fullscreen mode Exit fullscreen mode

But:

fmt.Println(user.Name)
Enter fullscreen mode Exit fullscreen mode

will panic because you're dereferencing a nil pointer.

So remember:

zero value
    ≠
always usable value
Enter fullscreen mode Exit fullscreen mode

The zero value is guaranteed and predictable, but its behavior depends on the type.

Practical Example: Backend Configuration

Suppose a service keeps some runtime statistics:

type Stats struct {
    Requests int64
    Errors   int64
    Healthy  bool
}
Enter fullscreen mode Exit fullscreen mode

Creating it requires nothing special:

var stats Stats
Enter fullscreen mode Exit fullscreen mode

Initially:

Requests = 0
Errors   = 0
Healthy  = false
Enter fullscreen mode Exit fullscreen mode

That can be exactly the state we want.

As requests arrive:

stats.Requests++
Enter fullscreen mode Exit fullscreen mode

There is no need for:

stats.Requests = 0
stats.Errors = 0
stats.Healthy = false
Enter fullscreen mode Exit fullscreen mode

Go already gives us that state.

This becomes especially useful with large structs containing counters, flags, slices, pointers, mutexes, and other components.

You only initialize values that genuinely need something different from their zero value.

When Should You Rely on Zero Values?

Rely on zero values when they naturally represent the initial state.

For example:

var attempts int
var finished bool
var lastError error
Enter fullscreen mode Exit fullscreen mode

These states are intuitive:

attempts  → 0
finished  → false
lastError → nil
Enter fullscreen mode Exit fullscreen mode

You can also design structs around this property.

Prefer:

type Metrics struct {
    Requests int64
    Errors   int64
}
Enter fullscreen mode Exit fullscreen mode

where:

var metrics Metrics
Enter fullscreen mode Exit fullscreen mode

already makes sense.

But don't force zero-value semantics when your type requires mandatory configuration.

For example:

type Client struct {
    baseURL string
}
Enter fullscreen mode Exit fullscreen mode

If a client cannot function without a URL, allowing this:

var client Client
Enter fullscreen mode Exit fullscreen mode

to appear valid may be misleading.

In that situation, a constructor can make more sense:

client := NewClient("https://api.example.com")
Enter fullscreen mode Exit fullscreen mode

The goal isn't to eliminate constructors.

The goal is to make the zero value useful when doing so produces a natural API.

Takeaways

  • Every Go variable has a defined value; there is no observable uninitialized state.
  • Numbers start at 0, booleans at false, strings at "", and many reference-like types at nil.
  • Struct fields recursively receive their own zero values.
  • A predictable zero state removes an entire class of uninitialized-state problems.
  • Good Go APIs often make their zero value useful, but not every zero value is safe for every operation.

Top comments (0)