3.2 Variables and Constants

Variables and constants are fundamental to any programming language. Go provides several ways to declare them, each with specific use cases.

Variable Declaration

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)
}

Type Inference

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

When 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

Zero Values

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

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

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

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