Constants and iota

A constant in Go is a named value that is fixed at compile time and can never change while the program runs. Because the compiler knows a constant’s value ahead of time, it can catch mistakes earlier, generate faster code, and let you use the same literal safely in many different numeric contexts. Go also has a special predeclared identifier, iota, that makes it easy to generate sequences of related constants — enumerated values, bit flags, byte-size units — without typing out 0, 1, 2, 3 by hand. This lesson covers the full const syntax, the difference between typed and untyped constants, and exactly how iota counts under the hood.

Overview: How Constants and iota Work

You declare a constant with the const keyword, either one at a time or grouped in a block:

const Pi = 3.14159

const (
	MaxRetries = 3
	TimeoutMS  = 500
)

Unlike a var, the right-hand side of a const declaration must be a constant expression: a literal, another constant, or an operation built entirely out of those. It cannot depend on anything known only at runtime — you cannot write const Now = time.Now(), because time.Now() is a function call that reads the system clock while the program is running, not something the compiler can compute ahead of time.

Go constants come in two flavors: typed and untyped. const AppName string = "LearnGo" is typed — it is always a string and can only be used where a string is expected. const MaxUsers = 100 is untyped — internally the compiler tracks it with arbitrary precision and no fixed type, and only converts it to a concrete type (its “default type”, such as int, float64, or string) at the point it is actually used. This is why the same untyped constant 3.14159 can be assigned to a float32 variable in one place and a float64 variable in another without any explicit conversion — something you cannot do with two differently-typed variables.

iota is a predeclared identifier that only has special meaning inside a const ( ... ) block. It represents a successive, untyped integer: it starts at 0 and is incremented by one for every constant specification (every line) in the block, whether or not that line actually mentions iota. When a line in a const block omits both a type and an expression, Go implicitly repeats the last non-empty expression from the previous line — that is what lets you write Sunday, Monday, Tuesday, ... or KB, MB, GB on separate lines with the formula written only once. Because Go has no built-in enum keyword, this iota pattern inside a const block, usually combined with a named type, is the idiomatic way to build enumerated constants.

Syntax

const identifier = expression

const (
	identifier1 = expression1
	identifier2 = expression2
)

const (
	identifier1 Type = iota // 0
	identifier2               // repeats "Type = iota", iota is now 1
)
Part Meaning
identifier The constant’s name, following normal Go naming rules (capitalized to export it from the package).
Type Optional. If given, the constant is typed; if omitted, it stays untyped until used.
expression Must be a constant expression: literals, other constants, and operators on them — never a function call or variable.
iota Only meaningful inside a const ( ... ) block; resets to 0 at each new block and increments by one per line.

Examples

Example 1: Basic typed and untyped constants

package main

import "fmt"

const Pi = 3.14159
const AppName string = "LearnGo"

func main() {
	const MaxUsers = 100
	fmt.Println(AppName, "version constant pi:", Pi, "max users:", MaxUsers)
}

Output:

LearnGo version constant pi: 3.14159 max users: 100

Pi and MaxUsers are untyped constants, while AppName is explicitly typed as string. Notice a constant can also be declared locally inside a function, just like a variable — it is simply scoped to that function.

Example 2: iota for an enumerated type

package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

func (d Weekday) String() string {
	names := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
	return names[d]
}

func main() {
	fmt.Println(Sunday, Wednesday, Saturday)
	fmt.Println(int(Wednesday))
}

Output:

Sunday Wednesday Saturday
3

Weekday is a named type built on int, and each weekday gets its value from iota (0 through 6). Because Weekday implements String(), satisfying the fmt.Stringer interface implicitly, fmt.Println automatically prints the readable name instead of the raw integer. Converting explicitly with int(Wednesday) bypasses that and prints the underlying number, 3.

Example 3: iota with a bit-shift formula for byte sizes

package main

import "fmt"

const (
	_  = iota
	KB = 1 << (10 * iota)
	MB
	GB
)

func main() {
	fmt.Println(KB, MB, GB)
}

Output:

1024 1048576 1073741824

The blank identifier _ discards the unwanted iota value of 0 so the sequence effectively starts at 1. Each subsequent line repeats the formula 1 << (10 * iota) with an incrementing iota, so KB is 1 KiB, MB is 1 MiB, and GB is 1 GiB — all computed once, at compile time.

How It Works Step by Step

Walking through the byte-size example line by line:

  1. The const ( keyword opens a new block, which resets iota to 0.
  2. Line 1, _ = iota: iota is 0; the value is assigned to the blank identifier and thrown away. Crucially, this line still counts — it consumes an iota step even though nothing is kept.
  3. Line 2, KB = 1 << (10 * iota): the block has advanced to its second constant specification, so iota is now 1. KB becomes 1 << 10, or 1024.
  4. Line 3, MB: no expression is written, so Go implicitly repeats 1 << (10 * iota) from the previous line, but with iota now at 2, giving 1 << 20, or 1048576.
  5. Line 4, GB: same implicit repetition, iota is 3, giving 1 << 30, or 1073741824.
  6. All four values are folded into the compiled binary as plain literals — there is no runtime shifting happening when main executes; the shifting was done by the compiler while evaluating the constant expressions.

Common Mistakes

Mistake 1: Assuming iota starts at 1

iota always starts at 0 in a new const block. If you need values that line up with an external system’s 1-based codes, this is an easy off-by-one bug:

// WRONG: assuming iota starts at 1
const (
	StatusPending = iota // actually 0, not 1
	StatusActive          // actually 1, not 2
	StatusDone             // actually 2, not 3
)
// If an external API expects codes 1, 2, 3, every value here is off by one.

Fix it by adding an offset to the first expression — the offset also propagates through implicit repetition:

package main

import "fmt"

const (
	StatusPending = iota + 1 // 1
	StatusActive             // 2
	StatusDone                // 3
)

func main() {
	fmt.Println(StatusPending, StatusActive, StatusDone)
}

Output:

1 2 3

Mistake 2: Trying to make a runtime value a constant

A const must be computable by the compiler. Assigning the result of a function call, even one that looks simple, fails to compile:

package main

import (
	"fmt"
	"time"
)

const StartTime = time.Now() // compile error: time.Now() is not a constant

func main() {
	fmt.Println(StartTime)
}

The fix is simply to use var (or :=) for anything determined while the program runs:

package main

import (
	"fmt"
	"time"
)

func main() {
	startTime := time.Now()
	fmt.Println(startTime.Year() > 2000)
}

Output:

true

Mistake 3: Expecting iota to continue across separate const blocks

iota resets to 0 every time a new const ( ... ) block begins — it does not remember where a previous block left off:

const (
	Read = iota // 0
	Write       // 1
)

const (
	Execute = iota // resets to 0, NOT 2 as some expect
	Delete          // 1, not 3
)
// fmt.Println(Read, Write, Execute, Delete) would print: 0 1 0 1 -- not 0 1 2 3

If the four values are meant to form one continuous sequence, they belong in the same block:

package main

import "fmt"

const (
	Read = iota
	Write
	Execute
	Delete
)

func main() {
	fmt.Println(Read, Write, Execute, Delete)
}

Output:

0 1 2 3

Best Practices

  • Use iota for any sequence of related constants (statuses, enums, flags) instead of hand-typing 0, 1, 2, 3 — it removes an entire class of typo bugs.
  • Give enum-like constants their own named type (type Weekday int) so the compiler stops you from passing an unrelated int where a Weekday is expected.
  • Implement String() (the fmt.Stringer interface) on iota-based types so logs and fmt.Println output show readable names instead of raw numbers.
  • Skip the zero value with _ = iota when zero shouldn’t be a meaningful state, so an uninitialized variable of that type is easy to detect as invalid rather than silently valid.
  • Never assume a second const block continues counting from a previous one — each block restarts iota at 0.
  • Prefer leaving widely reused numeric literals (like Pi or a retry count) untyped, so they adapt to whatever numeric type they’re used with; give an explicit type when the constant represents a specific domain concept.
  • Use grouped const blocks with iota to replace scattered magic numbers — it documents the whole set of related values in one place.

Practice Exercises

  • Exercise 1: Define a named type Direction with constants North, East, South, and West using iota, starting at 0. Print all four values with fmt.Println; without a String() method, expect the raw output 0 1 2 3.
  • Exercise 2: Using the bit-shift pattern from Example 3, define KB, MB, GB, and TB constants. Print TB and check it equals 1099511627776.
  • Exercise 3: Define file-permission-style flag constants ReadPerm, WritePerm, and ExecPerm using 1 << iota (so they are 1, 2, and 4). Combine two of them with the bitwise OR operator (|) and confirm you can test for a specific permission with bitwise AND (&).

Summary

  • A const declaration’s value must be a compile-time constant expression — never a function call or anything determined at runtime.
  • Constants can be untyped (flexible, adopt a type only when used) or explicitly typed (fixed to one type).
  • iota only has meaning inside a const ( ... ) block: it starts at 0 and increments by one per line, resetting to 0 in every new block.
  • A line that omits its expression implicitly repeats the previous line’s expression, re-evaluated with the new iota value.
  • iota is the idiomatic way to build enumerated constants and bit-flag sets in Go, since the language has no built-in enum keyword.
  • Give an enum-like constant group its own named type and a String() method for readable, type-safe output.