02 — Variables and Constants — Declaration, Zero Values, and iota
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 = 3The 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
[2] The Go Authors, "Go Tour: Short variable declarations," go.dev, 2024. [Online]. Available: 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
[4] The Go Authors, "Iota," Go Wiki, 2024. [Online]. Available: 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
Knowledge check · Question 1 of 5
Where can you use the short declaration `:=`?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!