Variables in Go are declared before use — no implicit globals, no hoisting. The language has two declaration forms: var (package or function scope, zero-initialized) and := (function scope only, inferred type). Understanding when to use each is one of the first things that makes Go code feel idiomatic.
For the type system details (numeric sizes, named types, type aliases) see Go type system basics. For constants and iota see Go constants and enumerations.
The Two Declaration Forms
var is the explicit form — works at package level and inside functions, requires the type or an initial value:
// Package-level variables — zero-initialized if no value given
var serverAddr = "localhost:8080"
var maxRetries int // 0 — zero value for int
// Inside functions
var name string = "Alice" // explicit type + value
var count = 42 // type inferred from value
var active bool // zero value: false
:= is the short declaration — only inside functions, always requires an initial value:
func main() {
name := "Alice" // type inferred as string
count := 42 // int
price := 19.99 // float64
active := true // bool
}
Use := inside functions (the common case). Use var when you need a zero value without an explicit initializer, or at package level.
Multiple Assignment
:= and var both assign multiple variables at once:
x, y := 10, 20
a, b, c := "hello", 42, true
// Swap without a temp variable
x, y = y, x
// Multiple return values
result, err := strconv.Atoi("42")
if err != nil {
log.Fatal(err)
}
:= in a multi-assignment requires at least one new variable on the left side. If all variables already exist, use = instead:
x := 1
x, y := 2, 3 // OK: y is new, x is reassigned
x, y = 4, 5 // OK: both already exist, no new vars
The Blank Identifier
_ discards a value — useful for return values you don’t need:
// Ignore the index in range
for _, value := range items {
process(value)
}
// Ignore an error you've explicitly decided to skip
// (use sparingly — ignoring errors silently is usually wrong)
data, _ := os.ReadFile("optional.json")
// Discard one of several return values
_, isAdmin := permissions["admin"]
The blank identifier also silences “declared but not used” compiler errors in special cases — though in practice, if you’re ignoring a variable, you probably shouldn’t have declared it.
Variable Scope
Go uses lexical scoping with block scope. A variable is visible from its declaration to the end of its enclosing block:
func example() {
x := 10 // outer x
if true {
y := 20 // only visible in this if block
x = 30 // modifies outer x
_ = y
}
// y is not accessible here
// := creates a NEW variable in the inner scope (shadowing)
if true {
x := 99 // NEW x, shadows outer x
_ = x // inner x
}
fmt.Println(x) // outer x is still 30
}
Shadowing is the most common source of subtle bugs with :=. The compiler won’t warn you — use go vet with -shadow or golangci-lint to catch unintentional shadowing.
Constants
Constants are values fixed at compile time. Untyped constants adapt to the context they’re used in:
const Pi = 3.14159 // untyped float constant
const MaxItems = 100 // untyped int constant
const ServiceName = "auth" // untyped string constant
// Grouped declaration
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
// Typed constants — participates in type checking
const MaxRetries int = 3
iota for Enumerations
iota auto-increments within a const block, enabling compact enumerations:
type Direction int
const (
North Direction = iota // 0
South // 1
East // 2
West // 3
)
// Bit flags
type Permission uint
const (
Read Permission = 1 << iota // 1 (binary: 001)
Write // 2 (binary: 010)
Execute // 4 (binary: 100)
)
// Skip the zero value so unset permissions aren't accidentally "Read"
const (
_ Permission = iota // 0 — unused
Read // 1
Write // 2
Execute // 4
)
iota resets to 0 at the start of each new const block. See Go constants and enumerations for the full pattern including String() methods.
Declaration Style Guide
Inside functions:
// ✅ Idiomatic: short declaration
name := "Alice"
count := 0
// ✅ When you need a zero value without an initial expression
var users []User // nil slice — will be appended to
var mu sync.Mutex // zero value is "unlocked" — useful directly
// ❌ Verbose when := would do
var name string = "Alice"
var count int = 0
Package level:
// ✅ var for mutable package state
var (
defaultTimeout = 30 * time.Second
maxPoolSize = 25
)
// ✅ const for fixed values
const version = "1.2.3"
Named return values (functions only):
// Useful for documenting what each return value means
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = fmt.Errorf("division by zero")
return // bare return uses named values
}
result = a / b
return
}
Type Inference Rules
The compiler infers types from literal values:
| Literal | Inferred type |
|---|---|
42 |
int |
3.14 |
float64 |
"hello" |
string |
true |
bool |
1 + 2i |
complex128 |
'A' |
rune (int32) |
When you need a specific type, convert explicitly:
var n int32 = int32(42) // explicit int32
f := float32(3.14) // explicit float32
Summary
- Use
:=inside functions (idiomatic); usevarat package level or when you want a zero value - Multiple assignment:
a, b := 1, 2— at least one variable must be new for:= _discards values you don’t need — blank identifier- Shadowing with
:=inside inner blocks is a common bug source — usego vetorgolangci-lintto catch it - Constants (
const) are fixed at compile time;iotaauto-increments within aconstblock - Untyped constants adapt to context; typed constants participate in type checking
Comments