Variables and var
Every value in a Go program lives inside a variable, and var is the keyword that creates one. Unlike dynamically typed languages, Go decides a variable’s type once — either from an explicit annotation or by inference from its initial value — and that type never changes for the life of the variable. Understanding var, its shorthand :=, and the zero-value guarantee behind them is the foundation for everything else you write in Go, from simple scripts to concurrent programs.
Overview: How Variables Work in Go
A variable in Go is a named piece of storage with a fixed type. The compiler tracks that type at compile time, which is how Go catches type errors — such as adding a string to an int — before your program ever runs, and how it generates efficient machine code without runtime type checks. You can create a variable in two ways: with the var keyword, which works at package level or inside a function, or with the short declaration operator :=, which works only inside a function body.
One of the biggest differences from C, Java, or Python is the zero value. When you declare a variable with var and give it no initial value, Go does not leave that memory as garbage or mark it “undefined.” It is automatically set to the zero value for its type, which removes an entire category of bugs — reading uninitialized memory — that plagues languages without this guarantee.
| Type | Zero Value |
|---|---|
Numeric types (int, float64, etc.) |
0 |
bool |
false |
string |
"" (empty string) |
| Pointers, slices, maps, channels, functions, interfaces | nil |
| Structs | every field set to its own zero value |
Declarations live at one of two scopes. Package-level variables are declared outside any function. They are visible throughout the package — and outside it too, if the name starts with a capital letter — and Go initializes them before main runs. The compiler works out the correct order automatically by analyzing which package-level variables reference which others, regardless of the order they appear in the source file. Local variables are declared inside a function body; they exist only while that function or block is executing, and every call gets a fresh set of them.
package main
import "fmt"
var appName string = "MyApp"
var version = "1.0.0"
func main() {
fmt.Println(appName, version)
}
Output:
MyApp 1.0.0
appName and version here are package-level variables, reachable from any function in the main package, and both already hold their values by the time main starts.
Under the hood, whether a variable’s storage ends up on the stack or the heap is decided by the compiler’s escape analysis, not by whether you wrote var or := — those are just two syntaxes for the same underlying declaration. If the compiler can prove a variable never needs to outlive its function call, it stays on the fast stack. If a pointer to it “escapes” — it’s returned from the function, stored in a global, or captured by a goroutine — the compiler allocates it on the heap instead so it survives. You never manage this by hand; it’s worth knowing because it explains why idiomatic Go code rarely revolves around manually avoiding allocations the way C does.
Syntax
Go offers four equivalent-looking forms for declaring a variable:
var name type = value // explicit type, explicit value
var name = value // type inferred from value
var name type // zero value, type explicit
name := value // short declaration, function body only
| Form | Where it’s valid | When to reach for it |
|---|---|---|
var x int = 5 |
package or function scope | you want both type and value spelled out, e.g. the inferred type would be wrong (a whole number you actually want as float64) |
var x = 5 |
package or function scope | the initial value already makes the type obvious |
var x int |
package or function scope | you want the zero value as a deliberate starting point |
x := 5 |
function scope only | the common case — quick, local, type inferred |
You can declare several variables in one statement, either on a single line (var a, b = 1, 2) or grouped in a parenthesized block, which is idiomatic once you have more than one or two package-level variables:
var (
width = 10
height = 20
)
A key rule for :=: at least one variable on the left-hand side must be new in the current scope. That lets you mix a brand-new variable with one that already exists — a pattern you’ll see constantly with error handling, where value, err := doSomething() is later followed by value2, err := doSomethingElse(), reusing the existing err instead of redeclaring it.
Examples
Example 1: Declaring variables and seeing zero values
This example declares four variables with no initial value, then prints them to show what Go fills them with automatically.
package main
import "fmt"
func main() {
var age int
var name string
var price float64
var active bool
fmt.Println(age, name, price, active)
var height int = 178
fmt.Println(height)
}
Output:
0 0 false
178
Notice the double space in the first line: fmt.Println separates every operand with a space, and name printed as an empty string, so there are spaces on both sides of it with nothing visible in between. None of these variables were left in an unpredictable state — int zeroed to 0, string to "", float64 to 0, and bool to false. The last line shows the explicit-type-and-value form, var height int = 178.
Example 2: Type inference, grouped declarations, and short declaration
This example mixes a grouped var block with the := short declaration to show that they produce identical results — the choice is about style and scope, not behavior.
package main
import "fmt"
func main() {
var (
width = 10
height = 20
)
area := width * height
x, y := 5, "five"
fmt.Println(area)
fmt.Println(x, y)
}
Output:
200
5 five
width and height are inferred as int from their untyped integer literals. area is declared with := because it’s local and the type (int) is obvious from multiplying two ints. The second short declaration, x, y := 5, "five", shows that := can declare several variables of different types at once, inferring int for x and string for y independently.
Example 3: A realistic example — temperature conversion
Here variables are used the way you’d actually use them in a small program: to hold an input, compute a derived value, and build up a result.
package main
import "fmt"
func main() {
var celsius float64 = 100
fahrenheit := celsius*9/5 + 32
var messages = []string{"Boiling point of water:"}
messages = append(messages, fmt.Sprintf("%.1fC = %.1fF", celsius, fahrenheit))
for _, m := range messages {
fmt.Println(m)
}
}
Output:
Boiling point of water:
100.0C = 212.0F
celsius is explicitly typed as float64 so the arithmetic stays in floating point; fahrenheit is inferred as float64 too, because it’s computed from celsius. messages starts as a []string built with a slice literal and grows with append, which is why the result of append must always be assigned back to a variable — the underlying array can change, and only the variable holding the slice header knows about the new one.
How It Works Step by Step
When your program starts, Go initializes things in a specific, well-defined order:
- Package-level
vardeclarations are evaluated first, in dependency order — if variableb‘s initializer reads variablea,ais guaranteed to be set first, no matter which one is written earlier in the file. - Any
init()functions in the package run next. main()begins executing.
Inside a function, declarations run top to bottom as ordinary statements. When execution reaches a var statement, Go reserves storage for the variable and fills it with the zero value (or the given initializer) at that point; the variable does not exist and cannot be referenced before that line runs. When execution reaches a := statement, the same thing happens, except the compiler must first look at the right-hand side’s type to decide what type to give the new variable — this all happens at compile time, so there is no runtime cost to inference.
Each time a function is called, its local variables get a fresh set of storage; recursive or repeated calls never share state between invocations unless you explicitly pass a pointer or use a variable declared outside the function.
Common Mistakes
1. Declaring a variable and never using it
Go treats an unused local variable as a compile error, not a warning. This keeps dead code from silently accumulating, but it surprises newcomers.
package main
import "fmt"
func main() {
var count int
fmt.Println("Starting up")
}
This fails with count declared and not used. The fix is simple: actually use the variable, or remove it if you don’t need it.
package main
import "fmt"
func main() {
var count int
count = 5
fmt.Println("Starting up, count =", count)
}
Output:
Starting up, count = 5
2. Writing to a nil map
The zero value of a map is nil, and reading from a nil map is perfectly safe — it just returns the zero value for the value type. Writing to one is not: it panics at runtime.
package main
import "fmt"
func main() {
var scores map[string]int
scores["alice"] = 90
fmt.Println(scores)
}
This compiles fine but panics with assignment to entry in nil map the instant it runs, because var scores map[string]int only declares a nil map header — no underlying storage exists yet. The fix is to initialize the map with make before writing to it.
package main
import "fmt"
func main() {
scores := make(map[string]int)
scores["alice"] = 90
fmt.Println(scores)
}
Output:
map[alice:90]
3. Accidentally shadowing a variable with :=
Because := creates new variables, using it inside an inner block (an if, a for, a nested block) with the same names as outer variables creates entirely new, separate variables that only exist in that inner block — the outer ones are never touched.
package main
import (
"errors"
"fmt"
)
func mightFail(fail bool) (int, error) {
if fail {
return 0, errors.New("failed")
}
return 42, nil
}
func main() {
value, err := 0, error(nil)
if true {
value, err := mightFail(true)
fmt.Println("inside:", value, err)
}
fmt.Println("outside:", value, err)
}
Output:
inside: 0 failed
outside: 0 <nil>
Even though mightFail returned an error, the outer err still reports <nil>, because the := inside the if block created a brand-new pair of value and err variables that shadow the outer ones and disappear when the block ends. The fix is to use plain assignment, =, so the existing outer variables are updated instead of shadowed.
package main
import (
"errors"
"fmt"
)
func mightFail(fail bool) (int, error) {
if fail {
return 0, errors.New("failed")
}
return 42, nil
}
func main() {
value, err := 0, error(nil)
if true {
value, err = mightFail(true)
fmt.Println("inside:", value, err)
}
fmt.Println("outside:", value, err)
}
Output:
inside: 0 failed
outside: 0 failed
Best Practices
- Prefer
:=inside functions for brevity, and reservevarfor package-level declarations, zero-value starting points, or when you need a type different from what would be inferred. - Group related package-level variables in a single
var (...)block instead of repeatingvaron every line. - Always initialize maps with
make(or a map literal) before writing to them — never assume a declared map is ready to use. - Never re-declare a variable with
:=inside a nested block just to “update” it; use=if the variable already exists in an outer scope. - Give variables names that describe what they hold, not their type —
count, notcountInt. - Keep variable scope as narrow as possible; declare a variable as close as you can to where it’s first used instead of at the top of a long function.
- Remember that
appendmay or may not allocate a new underlying array — always reassign its result to a variable, never discard it.
Practice Exercises
- Declare three variables — an
int, abool, and a[]string— usingvarwith no initial value, and print all three. Predict the output before you run it. - Write a function that declares an
errvariable with:=outside anifblock, then deliberately introduces the shadowing bug from Common Mistake 3 inside theif. Confirm the outererrstaysnil, then fix it with=. - Write a small program that declares
var scores map[string]intand tries to add an entry before callingmake. Read the panic message it produces, then fix it.
Summary
vardeclares a variable with an explicit type, an inferred type, or neither (the zero value) — and works at package or function scope.:=is shorthand forvarwith type inference, but only works inside a function body and requires at least one new variable on the left.- Every variable declared without an initializer gets its type’s zero value automatically —
0,false,"", ornil— so there is no such thing as reading uninitialized garbage in Go. - Package-level variables initialize before
mainruns, in dependency order, regardless of source order. - A nil map can be read safely but panics if you write to it — initialize with
makefirst. - Using
:=inside a nested block can silently shadow an outer variable of the same name; use=when you mean to update the outer one.
