---
title: "02 — Variables and Constants — Declaration, Zero Values, and iota"
uid: variables-and-constants
tags: ["golang", "iota", "scope", "variables", "constants", "roadmap:golang", "fundamentals"]
excerpt: "Go forces every variable to a real value from birth (zero values), splits declaration into var and :=, and uses iota for compile-time enumerations. Two forms, one rule about defaults."
date: 2026-08-13T03:28:13+0000
source: https://www.aveshina.my.id/en/blog/variables-and-constants
---

Two declaration syntaxes and a hard rule about defaults made Go's variables feel arbitrary at first. The model that finally stuck: **every variable in Go holds a real value from the moment it is declared, because unset variables get a deterministic zero value; and there are exactly two declaration forms, var and :=, split by where you are allowed to use them.** [1][2] Once I stopped fighting that split, declarations stopped being a guessing game.

## var vs := — the two declaration forms

Go offers two ways to bring a variable into existence, and the choice is mostly mechanical [2]:

```
var name string        // declared, zero value "" — works at package and function scope
var score = 42         // type inferred from the value

func main() {
    count := 10        // short declaration — function scope only
    name = "Ave"
}
```

The rule that resolves 90% of confusion: **:= only works inside a function body.** [2] At package scope (outside any function), var is mandatory. Inside a function, := is the idiomatic shorthand — it declares and assigns in one stroke and infers the type. Reach for var inside a function only when you want to declare without initializing (relying on the zero value) or when you need to state the type explicitly.

A second subtlety: := requires at least one new variable on its left-hand side. Reassignment to an existing variable uses =, not :=. Mixing them up is a common early compile error.

## Zero values — the reason "uninitialized" is safe

In some languages, an uninitialized variable holds garbage — whatever was in that memory. Go refuses to allow that. Every type has a **zero value**, and a declaration without an initializer gets it [3]:

- Numbers (int, float64, …): 0
- Boolean: false
- String: "" (empty)
- Pointers, slices, maps, channels, functions, interfaces: nil

This is not a footgun to avoid; it is a guarantee to lean on. A freshly declared map is nil, and reading a missing key returns the value type's zero value (more on that in the maps notes). A struct declared with var u User has every field at its zero value, which is why empty structs are safe to pass around. The discipline of "make the zero value useful" runs through the whole standard library — bytes.Buffer{}, sync.Mutex{}, and http.Server{} all work correctly with no explicit initialization [3].

## Constants and iota

Constants are declared with const and must be **compile-time determinable** [4]. They cannot be assigned the result of a function call or anything computed at runtime:

```
const Pi = 3.14159
const Greeting = "hello"
const MaxRetries = 3
```

The interesting half of constants is iota — a compile-time counter that resets to 0 at the top of each const block and increments by one per line [4]. It is how Go builds enumerations without an enum keyword:

```
type Weekday int

const (
    Sunday Weekday = iota   // 0
    Monday                  // 1
    Tuesday                 // 2
    Wednesday               // 3
    Thursday                // 4
    Friday                  // 5
    Saturday                // 6
)
```

The repetition of = iota is implicit — Go carries the type and expression down the block unless you override them. iota also powers bit-flag patterns, because the expression can use bitwise shifts: 1 << iota gives you 1, 2, 4, 8, … for combining flags. There is no enum keyword and no built-in stringification of the names; printing Monday gives 1, not "Monday". If you need names, you write a String() method or keep a parallel slice of strings.

## Scope and shadowing

Scope is where Go gets quietly dangerous. A variable is visible from the point it is declared down to the end of its **block** (the { } that encloses it). Go has three scope levels: the package, the file, and the block [5].

The trap is **shadowing** — declaring a new variable in an inner block with the same name as an outer one, which hides the outer variable for the duration of the inner block [5]:

```
func update() {
    count := 1
    if true {
        count := 99   // NEW variable, shadows the outer count
        fmt.Println(count) // 99
    }
    fmt.Println(count)     // 1 — outer count untouched
}
```

That inner count := 99 does not reassign; it declares a brand-new count that ceases to exist at the closing brace. The outer count is never modified. go vet will flag the most egregious shadowing cases, but the core defense is awareness: when you meant to reassign, use =, and be careful with := inside if and for blocks where a new name collides with one from outside.

## How I use this

Two habits came directly from these notes. First, I default to := inside functions and reserve var for package-level declarations and the rare case where I want the zero value explicitly — that single rule removes most decision overhead. Second, I treat the zero value as a feature: I design structs so their zero value is a valid, usable state, the way bytes.Buffer does. That makes initialization optional and removes a whole class of "did I remember to set this?" bugs. And when a variable silently refuses to change, the first thing I check is whether an inner := shadowed it.

## References

[1] The Go Authors, "Go Tour: Variables," go.dev, 2024. [Online]. Available: [https://go.dev/tour/basics/8](https://go.dev/tour/basics/8)

[2] The Go Authors, "Go Tour: Short variable declarations," go.dev, 2024. [Online]. Available: [https://go.dev/tour/basics/10](https://go.dev/tour/basics/10)

[3] The Go Authors, "Go Tour: Zero values," go.dev, 2024. [Online]. Available: [https://go.dev/tour/basics/12](https://go.dev/tour/basics/12)

[4] The Go Authors, "Iota," Go Wiki, 2024. [Online]. Available: [https://go.dev/wiki/Iota](https://go.dev/wiki/Iota)

[5] S. Shahin, "Variable Shadowing in Go: Best Practices to Avoid Confusions and Bugs," Medium, 2024. [Online]. Available: [https://medium.com/@shahpershahin/variable-shadowing-in-go-best-practices-to-avoid-confusions-and-bugs-61e03022b54d](https://medium.com/@shahpershahin/variable-shadowing-in-go-best-practices-to-avoid-confusions-and-bugs-61e03022b54d)

```quiz
Q: Where can you use the short declaration `:=`?
- Anywhere — package scope or inside functions
- Only inside function bodies
correct: 1
explain: `:=` is restricted to function scope. At package level (outside any function) you must use `var`.

Q: What is the zero value of a freshly declared `var m map[string]int`?
- An empty, ready-to-use map
- nil
correct: 1
explain: A declared-but-unmade map is nil. Reading a missing key returns the value type's zero value, but writing to a nil map panics — you must `make` it first.

Q: What does `iota` do inside a `const` block?
- Generates a unique random integer per constant at runtime
- Provides a compile-time counter starting at 0 that increments by 1 each line and resets per const block
correct: 1
explain: iota is a compile-time constant generator. It starts at 0 in each const block and steps up by one per line, enabling enumerations and bit-flag patterns.

Q: In the snippet, what does the outer `count` hold after the if-block runs?
- 1, because the inner `count := 99` declares a new variable that shadows and does not modify the outer one
- 99, because the inner assignment overwrote it
correct: 0
explain: `:=` inside the if-block creates a separate variable that shadows the outer count and dies at the brace. The outer count is never touched.

Q: A Go constant declared with `const` must be…
- computed at runtime from a function call
- determinable at compile time
correct: 1
explain: Constants are compile-time values. You cannot assign them the result of a function call or any runtime computation.
```
