Variable Declarations

How to declare variables in Go using the var statement and short variable declarations, including zero values, scope, and shadowing

A variable is a named storage location that holds a value your program can use and change. In Go, every variable belongs to a fixed type that is determined when the variable is created — either you write the type explicitly, or the compiler infers it from the value you assign. This static typing catches entire categories of bugs before the program ever runs.

Go gives you two main ways to create a variable: the var keyword and the short declaration operator :=. They are not interchangeable, and picking the right one in a given situation is a skill that separates readable Go from noisy Go. Underneath both mechanisms sits a safety net called zero values — Go will never let you read an uninitialized variable; it always has a predictable starting value. And finally, where you declare a variable determines its scope, which introduces the subtle pitfall of shadowing.

All of these pieces fit together. The next sections will walk through each.

The var Statement

The var keyword declares a variable with an explicit name and, optionally, a type and an initial value. It works at both package level and inside functions.

var temperature float64 = 23.5
var city = "Lisbon"
var isReady bool

You can omit the type if you initialize the variable immediately — Go will infer it from the value. If you omit the initialization, you must keep the type, and the variable gets the zero value for that type.

You can also group multiple var declarations into a factored block, which is common at package level:

var (
    maxWorkers   int    = 10
    defaultName  string = "guest"
    retryTimeout        = 2 * time.Second
)

The block groups related configuration and signals that these variables belong together. Inside a block, you can mix explicit and inferred types freely.

var works everywhere:

Unlike the short := declaration, var is legal at package level. When you need a variable that lasts the entire program lifetime and isn't tied to one function, var is your only option.

When you see var with no initialization, read it as a clear signal: "I need this variable to exist now, and its starting value will be the language-defined zero."

Short Variable Declarations

Inside a function, you can use := to declare and initialize a variable in one compact form. The type is inferred from the value on the right.

count := 0
name := "Faro"
price := 12.99

This is the idiomatic way to create most local variables in Go. It removes repetition and lets the code focus on what matters: the name and the value.

You can declare several variables at once, mixing types, as long as the number of values matches the number of names:

file, err := os.Open("data.txt")
x, y := 10, 42

The := operator is a declaration with initialization, not an assignment. That distinction matters because := expects at least one new variable on the left-hand side. If all the variables already exist in the same scope, you get a compilation error.

Package-level := is illegal:

Using := outside a function body produces a syntax error. If you need a package-level variable, use var. This is one of the most common surprises for newcomers.

A := line also shadows an outer variable if the name already exists in an enclosing scope — we cover that in the section on scope and shadowing later.

Choosing Between var and :=

Both forms create variables, but they serve different purposes in idiomatic Go.

SituationUse
Package-level variablevar (only option)
Local variable with an initial value:=
Local variable without a value yetvar x T (explicit zero)
Grouping related declarationsvar (...)
Reinforcing the type matters for readabilityvar with explicit type
Keeping local code concise:=

A common beginner habit is reaching for var inside functions even when initializing immediately. That leads to extra tokens that distract from the logic. Prefer := for local initialization; reach for var when you need package scope, delayed initialization, or an explicit type reminder.

Zero Values

Go automatically assigns a well-defined starting value to every variable that is declared without an explicit initialization. There is no "undefined" or "null" in the sense of uninitialized memory.

The zero values for basic types:

  • int, float64, and all numeric types: 0
  • bool: false
  • string: "" (the empty string)
  • Pointers, slices, maps, channels, interfaces, and functions: nil
var quantity int     // 0
var active bool      // false
var label string     // ""
var items []string   // nil

This design means a freshly declared variable is always usable. A nil slice has a length of zero and can be appended to without allocation. A nil map, on the other hand, causes a runtime panic if you try to write to it — so you need to initialize maps with make before use.

nil maps read safely but write dangerously:

Reading from a nil map returns the zero value of the element type, but assigning to a nil map panics. This is a common gotcha: always make your maps before inserting data.

Zero values remove an entire class of bugs that plague languages where uninitialized variables contain garbage. You never have to guess what a variable holds before the first assignment — the language guarantees it.

Scope and Shadowing

A variable's scope is the region of source code where it can be referenced by name. Go uses lexical scoping with blocks: a variable declared inside a function lives from its declaration to the end of the enclosing curly braces. A variable declared at package level is visible throughout the package.

Shadowing occurs when you declare a new variable with the same name in an inner block. The inner variable hides the outer one for the duration of that block.

package main
import "fmt"
var message = "package scope"
func main() {
    fmt.Println(message) // prints "package scope"
    if true {
        message := "block scope" // new variable, shadows the package-level one
        fmt.Println(message)     // prints "block scope"
    }
    fmt.Println(message) // prints "package scope" again
}

The := operator is particularly prone to accidental shadowing because it creates a new variable without ceremony. If you intended to assign to an existing variable but typed := instead of =, you get a new variable in the current scope that disappears afterward — and the outer variable remains unchanged.

Shadowing can mask bugs silently:

Using := when you meant = is a compile-time valid mistake that can introduce subtle logic errors. A common defense is to limit block sizes and avoid reusing variable names inside nested scopes. Tools like go vet can catch some shadowing cases.

A special form of := allows redeclaration of variables that already exist in the same scope, but only if at least one new variable is being created. This is frequently used with err in error handling patterns:

f, err := os.Open("first.txt")
// ...
g, err := os.Open("second.txt") // err is reassigned, not redeclared

This works because g is new, so err is reused. It keeps error handling compact without polluting the scope with err2, err3 names.

Short redeclaration is safe when one variable is new:

As long as the := line introduces at least one fresh name, the compiler treats the other names as assignments. This is the intended pattern for chained error handling in Go.

Summary

Go's variable system is built on three layers: declaration (how you bring a variable into existence), initialization (what value it starts with), and scope (where that variable is visible). The language gives you two declaration tools — var for explicitness and package-level variables, := for concise local initialization — and a deterministic zero-value guarantee that removes uninitialized memory from the equation.

The most important habit to form early is choosing the right declaration form for the situation, not arbitrarily. Use := as your default inside functions, var when you need package scope or delayed initialization, and const when the value should never change (covered in the Constants section). Pay attention to scope boundaries; every curly brace creates an opportunity for shadowing if you're not deliberate about name reuse.