CHAPTER 05 · CORE LANGUAGE

Variables & Constants

A program needs somewhere to put its data. In Go, that "somewhere" is a variable if it can change, or a constant if it can't. Go gives you a few different syntaxes for declaring each; this chapter walks through them and the small surprises that come with each one.

Learning objectives

  • Declare variables with var and the short form :=.
  • Know which form is allowed where (and why).
  • Recall Go's zero values for every basic type.
  • Use the blank identifier _ to discard values intentionally.
  • Define constants with const and use iota for enums.
  • Distinguish typed from untyped constants.

Declaring with var

The most explicit form. You name the variable, then optionally specify the type and/or an initial value:

package main

import "fmt"

func main() {
    var name string                  // declared, no value (zero value: "")
    var age int = 30                 // type AND value
    var country = "USA"              // value only, type is inferred
    var height float64 = 1.78

    fmt.Println(name, age, country, height)
}

All four lines are valid. Most idiomatic Go uses the third form (no explicit type) when an initializer is present. Go can infer the type, and the explicit annotation just adds noise.

You can also declare multiple variables in a single var block, which is common at the package level:

var (
    appName    = "greet"
    appVersion = "1.0.0"
    debug      = false
)

Short declaration: :=

Inside a function, the most common way to declare and initialize a variable is the short form:

city := "San Francisco"
population := 883_305       // underscores allowed in numeric literals

The type is inferred from the right-hand side. := is sweet and short, you'll use it constantly. Two rules:

  1. Function-scoped only. You can't use := at the package (top) level. Use var there.
  2. At least one new name on the left. If all variables on the left already exist, use = to assign instead of :=.
x := 1            // declare
x = 2             // assign, already declared

x, y := 3, 4      // OK: y is new
y, z := 5, 6      // OK: z is new (y is reused)
y = 7             // OK: just assignment
// y, x := 8, 9   // ❌ both already declared: won't compile
!
Unused variables are compile errors Declare a local variable and never read it? Build fails. This stops dead code from accumulating, but in the moment it's annoying, use the blank identifier _ (below) to silence the compiler when you genuinely want to discard a value.

Multiple declarations

Both forms support multiple names at once:

var x, y, z int = 1, 2, 3
name, age := "John", 25

You can also swap two variables in one line, no temp variable needed:

a, b := 1, 2
a, b = b, a       // a == 2, b == 1
fmt.Println(a, b)

Zero values

Unlike many languages, Go has no concept of "uninitialized" memory in user code. Every declared variable gets a sensible default, its zero value:

TypeZero value
Numeric (int, float64, …)0
boolfalse
string"" (empty)
Pointers, interfaces, slices, maps, channels, functionsnil
StructsEach field zero-valued
ArraysEach element zero-valued

This means var x int is safe to use immediately, no garbage value, no segfault. It's a small thing that prevents a surprising number of bugs.

var (
    n int
    s string
    ok bool
)
fmt.Printf("%d %q %t\n", n, s, ok)   // prints: 0 "" false

The blank identifier _

Sometimes a function returns more values than you care about. Assign the throwaways to _:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    n, _ := strconv.Atoi("42")    // we don't care about the error here
    fmt.Println(n)
}

You can also use _ to import a package purely for its side effects (its init functions run; nothing else):

import _ "image/png"   // registers the PNG decoder; we don't call it directly
!
Don't ignore errors casually _ on an error return is convenient, but it's a footgun in production code. Get in the habit of handling errors (Chapter 23). Reach for _ only when you genuinely don't need the value , examples, throwaway scripts, or known-safe contexts.

Constants

A const is a value fixed at compile time. It can never be reassigned:

const Pi = 3.14159
const Greeting = "Hello, Go!"
const MaxRetries = 3

// Pi = 3.14   // ❌ won't compile: cannot assign to Pi

Constants must be initialized at declaration. Their value must be a compile-time constant expression: you can't say const Now = time.Now(), because time.Now() is a function call evaluated at run time.

You can group constants like variables:

const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)

iota: Go's constant generator

Inside a const ( … ) block, iota is a special identifier that starts at 0 and increments by one for every constant in the block. It's how Go does enums:

package main

import "fmt"

type Weekday int

const (
    Sunday    Weekday = iota   // 0
    Monday                     // 1
    Tuesday                    // 2
    Wednesday                  // 3
    Thursday                   // 4
    Friday                     // 5
    Saturday                   // 6
)

func main() {
    today := Wednesday
    fmt.Println("today is", today)   // prints: today is 3
}

iota resets to 0 at the top of every const block. You can use expressions:

const (
    KB = 1 << (10 * (iota + 1))  // 1 << 10 == 1024
    MB                            // 1 << 20
    GB                            // 1 << 30
    TB                            // 1 << 40
)

The iota pattern is everywhere in Go's standard library: HTTP status codes, file open modes, log levels, you name it.

Typed vs untyped constants

A subtle but important detail. By default, constants are untyped:

const x = 100        // untyped numeric
var i int = x        // OK, x adapts to int
var f float64 = x    // OK: x adapts to float64

Untyped constants take on the type they need at the point of use. They're like flexible literals.

If you write const x int = 100, you've made x a typed constant of type int. Now you can't assign it to a float64 without an explicit conversion:

const x int = 100
var f float64 = x          // ❌ cannot use x (type int) as type float64
var f float64 = float64(x) // ✓ explicit conversion

For most everyday constants (numbers, strings), leave them untyped. You get more flexibility for free.

A peek at scope

Where you declare a variable determines where you can use it:

  • Package-level declarations live outside any function and are visible everywhere in that package.
  • Function-level declarations are visible only inside that function (and any nested blocks within it).
package main

import "fmt"

var greeting = "hi"           // package-level

func main() {
    name := "Alice"           // function-level

    if true {
        place := "Mars"       // block-level: only inside the if
        fmt.Println(greeting, name, place)
    }
    // fmt.Println(place)     // ❌ undefined: place
}

Chapter 17 has the full story on scope and the famous "shadowing" pitfall. For now, just notice that variables only exist in the block they're declared in.

Check your understanding

Practice exercises

EXERCISE 1

Spot the zero values

Without running it, predict what this program prints. Then run it and check.

package main

import "fmt"

func main() {
    var (
        n    int
        f    float64
        s    string
        ok   bool
        list []int
    )
    fmt.Printf("%d %g %q %t %v\n", n, f, s, ok, list)
    fmt.Println("list is nil?", list == nil)
}
Show expected output
0 0 "" false []
list is nil? true

Note that list prints as [] but is in fact nil. We'll see why in Chapter 13: it's a quirk of how slices print.

EXERCISE 2

Build a tiny enum with iota

Define a Severity type with four constants: Debug, Info, Warn, Error using iota. Then write a function that takes a Severity and returns a string label (don't worry about implementing String() properly yet, a simple switch is fine).

Show one possible solution
package main

import "fmt"

type Severity int

const (
    Debug Severity = iota
    Info
    Warn
    Error
)

func label(s Severity) string {
    switch s {
    case Debug:
        return "DEBUG"
    case Info:
        return "INFO"
    case Warn:
        return "WARN"
    case Error:
        return "ERROR"
    default:
        return "UNKNOWN"
    }
}

func main() {
    levels := []Severity{Debug, Info, Warn, Error}
    for _, l := range levels {
        fmt.Println(label(l))
    }
}

In Chapter 21 you'll learn to attach a String() method to Severity so that fmt.Println(s) prints the label automatically.

Further reading

Variables and constants down, onto types.