Variables and constants are fundamental to any programming language. Go provides several ways to declare them, each with specific use cases.
Go offers multiple ways to declare variables:
1. Full declaration with type
var name string = "Go"
var age int = 10
var isActive bool = true
This syntax explicitly declares the variable name, its type, and optionally an initial value. The var keyword signals a variable declaration, followed by the variable name, the type, and optionally an assignment with =. When you provide an initial value, Go ensures it matches the declared type. This form is most useful when you want to be explicit about types or when declaring variables without initialization (they'll receive their zero values).
2. Type inference (type can be omitted)
var name = "Go" // Inferred as string
var version = 1.23 // Inferred as float64
var count = 42 // Inferred as int
When you provide an initial value, Go can infer the type from that value, allowing you to omit the explicit type declaration. The compiler analyzes the literal or expression on the right side of = and assigns the appropriate type. This reduces verbosity while maintaining type safety—the variable still has a specific type, it's just determined automatically. Integer literals default to int, floating-point literals to float64, and string literals to string.
3. Multiple variable declaration
var (
name string = "Go"
version float64 = 1.23
active bool = true
)
The grouped syntax uses parentheses to declare multiple variables together. This is particularly useful for package-level variables or when you want to organize related declarations. Each variable in the group follows the same rules as individual declarations—you can specify types explicitly, use type inference, or declare without initialization. The grouping makes it easy to see all related variables at once and is commonly used at the package level.
4. Short variable declaration (most common)
name := "Go"
version := 1.23
count := 42
The := operator combines declaration and assignment in a single statement. This is the most concise form and is preferred inside functions. The operator declares new variables and assigns values to them, with types inferred from the assigned values. Importantly, := can only be used inside functions, not at the package level. It can also declare multiple variables at once: x, y := 1, 2 declares both x and y with their respective values.
Complete example
package main
import "fmt"
func main() {
// Full declaration
var name string = "Go"
var version = 1.23 // Type inference
// Short variable declaration
language := "GoLang"
// Multiple variables
var x, y int = 1, 2
a, b := 3, 4
// Variable swapping (Go idiom)
x, y = y, x
fmt.Println(name, version, language)
fmt.Println(x, y, a, b)
}
Go's compiler can infer types from the assigned value, making code more concise while maintaining type safety. This feature eliminates unnecessary boilerplate without compromising the type system's guarantees.
Type inference reduces verbosity in a way that makes code cleaner and easier to read. Instead of explicitly writing var x int = 42, you can write var x = 42 and let Go determine that x is an int. This is particularly useful when the type is obvious from the value, allowing you to focus on the logic rather than repetitive type declarations.
When you declare a variable with var and provide an initial value, Go's compiler analyzes the value's type and assigns that type to the variable. This analysis happens entirely at compile time, so there's no runtime overhead—the type is fully determined before your program runs. The compiler determines the type based on the literal's default type or the expression's result type, following consistent rules that make the inference predictable.
var x = 42 // int (integer literal defaults to int)
var y = 3.14 // float64 (floating-point literal defaults to float64)
var z = "hello" // string (string literal is string)
var w = true // bool (boolean literal is bool)
Type inference rules
int (platform-dependent size, typically 64 bits on modern systems)float64 (double precision)stringboolWhen to specify types explicitly
// When you need a specific size
var count int32 = 1000 // Need 32-bit integer
var price float32 = 99.99 // Need 32-bit float
// When declaring without initialization
var name string // Type must be explicit if no value
var age int
// When the inferred type might not be what you want
var x = 42 // int, but if you need int64:
var y int64 = 42 // Explicit type required
In Go, variables are automatically initialized to their "zero value" if not explicitly initialized. This fundamental design decision makes Go programs safer and more predictable by eliminating an entire class of bugs that plague other languages.
Many languages leave uninitialized variables with undefined or garbage values, creating opportunities for subtle bugs that can be difficult to track down. Go's zero value system ensures that every variable has a well-defined, safe initial state from the moment it's declared. This predictability makes code easier to reason about and eliminates the uncertainty that comes with uninitialized memory.
When you declare a variable without an initial value, Go automatically sets it to the zero value for that type. This initialization happens at the point of declaration, before any code can access the variable, guaranteeing that you'll never encounter an undefined state. The zero value isn't just "empty"—it's a meaningful default that represents the "empty" or "unset" state for that type, making it safe to use in many contexts without explicit initialization.
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
var p *int
fmt.Printf("int: %d\n", i) // 0
fmt.Printf("float64: %f\n", f) // 0.0
fmt.Printf("bool: %t\n", b) // false
fmt.Printf("string: %q\n", s) // ""
fmt.Printf("pointer: %v\n", p) // <nil>
}
Zero values by type
0 (a meaningful default for numbers)false (the logical "off" state)"" (empty string, not null)nil (represents "no value")Zero values enable you to write code that works correctly even when variables aren't explicitly initialized. A counter starting at 0 is often exactly what you want, and a boolean starting at false provides a safe default that represents the logical "off" state. This design allows you to write more concise code while maintaining safety.
Practical use
// Zero values make initialization safe
var numbers []int // nil slice, safe to check with len() == 0
var m map[string]int // nil map, must initialize with make() before use
// Check for zero value (common pattern)
if name == "" {
name = "Default"
}
// Zero values work in structs too
type Person struct {
Name string // ""
Age int // 0
}
var p Person // All fields are zero values
Constants are immutable values declared with the const keyword:
const pi = 3.14159
const greeting = "Hello, World!"
const maxSize = 1024
Typed constants
const pi float64 = 3.14159
const maxInt int = 2147483647
Multiple constants
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
Constant expressions
const (
secondsPerMinute = 60
minutesPerHour = 60
secondsPerHour = secondsPerMinute * minutesPerHour
)
Complete example
package main
import "fmt"
const (
StatusOK = 200
StatusNotFound = 404
pi = 3.14159
)
func main() {
fmt.Println("Status OK:", StatusOK)
fmt.Println("Pi:", pi)
// Constants can be used in expressions
radius := 5.0
area := pi * radius * radius
fmt.Printf("Area of circle with radius %.2f: %.2f\n", radius, area)
}
iota is a special constant generator that creates a sequence of related constants. It's a powerful feature for defining enumerations and related constant values.
Manually assigning sequential values to constants is error-prone and tedious. If you need to add a constant in the middle of a sequence, you'd have to renumber all subsequent constants, which is both time-consuming and prone to mistakes. iota solves this problem by automatically generating sequential values, making it easy to add, remove, or reorder constants without manual renumbering.
iota is a predeclared identifier that represents an untyped integer constant. It starts at 0 in each const block and increments by 1 for each constant declaration in that block. When a new const block begins, iota resets to 0, allowing you to create independent sequences. The compiler replaces iota with the current value during constant evaluation, so this all happens at compile time with no runtime cost.
const (
Sunday = iota // 0 (iota starts at 0)
Monday // 1 (iota increments)
Tuesday // 2 (iota increments)
Wednesday // 3 (iota increments)
Thursday // 4 (iota increments)
Friday // 5 (iota increments)
Saturday // 6 (iota increments)
)
When Go encounters iota in a constant declaration, it uses the current value of iota for that constant. Then, for the next constant in the same block, iota is automatically incremented. This process continues for each subsequent constant in the block, all happening at compile time with no runtime overhead.
Iota with expressions
const (
_ = iota // 0 (ignored)
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
TB // 1099511627776
)
Iota in separate blocks
const (
A = iota // 0
B // 1
)
const (
C = iota // 0 (resets)
D // 1
)
Practical iota examples
package main
import "fmt"
// File permissions
const (
ReadPermission = 1 << iota // 1
WritePermission // 2
ExecutePermission // 4
)
// Log levels
const (
LogDebug = iota // 0
LogInfo // 1
LogWarning // 2
LogError // 3
)
func main() {
fmt.Printf("Read: %d, Write: %d, Execute: %d\n",
ReadPermission, WritePermission, ExecutePermission)
fmt.Printf("Debug: %d, Info: %d, Warning: %d, Error: %d\n",
LogDebug, LogInfo, LogWarning, LogError)
}
Key points about variables and constants
const for values that never changeiota for related sequential constants:=) are idiomatic in Go