Variables in Go
Declare variables with var and :=, understand zero values, type inference, multiple assignment, and the blank identifier.
Declaring Variables
Go gives you two ways to declare variables: the var keyword and the short declaration :=. Choosing between them is mostly about context — var works everywhere and is explicit about types, while := is concise and idiomatic inside functions.
Using var
The var keyword is the most explicit form. You can specify the type, the initial value, or both. When you omit the initial value, Go automatically sets the variable to its zero value — a key guarantee that eliminates uninitialized variable bugs.
// var name type = value
var age int = 25
var name string = "Alice"
var active bool = true
// Type can be omitted when Go can infer it from the value
var score = 98.5 // inferred as float64
// Declaration without initialization — gets zero value automatically
var count int // count == 0
var label string // label == ""
var flag bool // flag == false
Using := (short declaration)
Inside a function, := is the idiomatic way to declare and assign in one step. It infers the type from the right-hand side, which keeps code concise without sacrificing the safety of static typing. You’ll use this form for the vast majority of local variables.
func main() {
age := 25 // int
name := "Alice" // string
score := 98.5 // float64
active := true // bool
fmt.Println(age, name, score, active)
}
:= is not available at the package level — only inside functions.
Zero Values
Every type in Go has a well-defined zero value. When you declare a variable without an explicit initial value, Go sets it to that zero value automatically. This design choice means you never have to worry about reading garbage memory from an uninitialized variable — a whole class of bugs that plagues languages like C simply does not exist in Go.
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string)
var p *int // nil
var sl []int // nil
var m map[string]int // nil
Because zero values are safe to read, you can write accumulator logic without explicit initialization:
var total int
for i := 0; i < 5; i++ {
total += i // safe — total starts at 0, no initialization needed
}
fmt.Println(total) // 10
Multiple Assignment
Go lets you declare or assign multiple variables in a single statement. This reduces verbosity for related values and, more importantly, enables a clean pattern for functions that return multiple values — which is how Go handles errors throughout the standard library.
// Multiple var declarations
var x, y, z int = 1, 2, 3
// Multiple short declarations
a, b := 10, 20
fmt.Println(a, b) // 10 20
// Swap without a temporary variable — multiple assignment makes this elegant
a, b = b, a
fmt.Println(a, b) // 20 10
Multiple assignment is particularly useful with functions that return multiple values:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Both return values are captured in a single statement
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%.4f\n", result) // 3.3333
The Blank Identifier _
Go’s compiler enforces that every declared local variable must be used — declaring a variable and never reading it is a compile error. This prevents dead code from accumulating in codebases. The blank identifier _ is the escape hatch: it discards a value explicitly, signaling to both the compiler and to readers that the value is intentionally ignored.
// Ignore the error (not recommended in production code)
result, _ := divide(10, 2)
// Ignore the index in a range loop when you only need the value
names := []string{"Alice", "Bob", "Charlie"}
for _, name := range names {
fmt.Println(name)
}
// Import a package only for its side effects (registers HTTP handlers, etc.)
import _ "net/http/pprof" // registers pprof HTTP handlers as a side effect
var Block Declarations
When you have several related package-level variables, grouping them in a var block makes the code easier to read and shows that the variables are logically connected. This is a common pattern for configuration defaults and package-level state.
var (
host = "localhost"
port = 8080
timeout = 30 * time.Second
debug = false
)
This is common for package-level configuration variables.
Constants
Constants are for values that are known at compile time and never change. They give meaningful names to magic numbers, prevent accidental modification, and allow the compiler to catch misuse. The iota enumerator makes it easy to define sequences of related constants without repeating numbers manually.
const Pi = 3.14159
const MaxRetries = 3
const AppName = "myapp"
// Typed constant — can only be used where that type is expected
const MaxSize int = 1024
// iota — auto-incrementing integer constant, resets to 0 in each const block
type Direction int
const (
North Direction = iota // 0
East // 1
South // 2
West // 3
)
// iota with an expression — useful for bit sizes
const (
KB = 1 << (10 * (iota + 1)) // 1024
MB // 1048576
GB // 1073741824
)
Type Inference Rules
Go’s type inference is deterministic — the same literal always produces the same type. Understanding these rules helps you predict what type you get from := and avoid subtle mismatches when passing values to functions that expect a specific type.
x := 42 // int (not int32 or int64 — untyped int defaults to int)
y := 3.14 // float64
z := "hello" // string
w := true // bool
c := 'A' // rune (int32) — single quotes make a rune, not a string
// Untyped constants are flexible — they take the type of the context they're used in
const bigNum = 1_000_000_000_000 // untyped constant, fits any integer type
var i64 int64 = bigNum // fine
var i32 int32 = bigNum // fine too
Short Declaration Gotcha — Redeclaration
:= requires at least one new variable on the left side. If all variables already exist in scope, you must use =. This rule trips up beginners when they try to assign to an existing variable alongside a new one.
x := 10
y := 20
// This works because z is new — x is reassigned, z is newly declared
x, z := 30, 40
// This would be a compile error — no new variables on the left side
// x, y := 30, 40 // error: no new variables on left side of :=
Package-Level Variables
Variables declared outside functions are package-level. They are initialized before main() runs, making them available to all functions in the package. However, package-level mutable state makes code harder to test and reason about — prefer passing values through function parameters whenever possible.
package main
import "fmt"
var greeting = "Hello" // package-level, accessible by all functions in the package
func main() {
fmt.Println(greeting) // Hello
greet("Gopher")
}
func greet(name string) {
fmt.Printf("%s, %s!\n", greeting, name)
}
Use package-level variables sparingly — they make code harder to test. Prefer passing values through function parameters.
Practical Example
package main
import (
"fmt"
"math"
)
// Named return values document what each value represents
func circleMetrics(radius float64) (area, circumference float64) {
area = math.Pi * radius * radius
circumference = 2 * math.Pi * radius
return
}
func main() {
const radius = 5.0 // const for a value that never changes
area, circ := circleMetrics(radius) // := captures both return values
fmt.Printf("Radius: %.2f\n", radius)
fmt.Printf("Area: %.4f\n", area)
fmt.Printf("Circumference: %.4f\n", circ)
}
Output:
Radius: 5.00
Area: 78.5398
Circumference: 31.4159