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:
- The
const (keyword opens a new block, which resetsiotato0. - Line 1,
_ = iota:iotais0; the value is assigned to the blank identifier and thrown away. Crucially, this line still counts — it consumes aniotastep even though nothing is kept. - Line 2,
KB = 1 << (10 * iota): the block has advanced to its second constant specification, soiotais now1.KBbecomes1 << 10, or1024. - Line 3,
MB: no expression is written, so Go implicitly repeats1 << (10 * iota)from the previous line, but withiotanow at2, giving1 << 20, or1048576. - Line 4,
GB: same implicit repetition,iotais3, giving1 << 30, or1073741824. - All four values are folded into the compiled binary as plain literals — there is no runtime shifting happening when
mainexecutes; 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
iotafor 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 unrelatedintwhere aWeekdayis expected. - Implement
String()(thefmt.Stringerinterface) oniota-based types so logs andfmt.Printlnoutput show readable names instead of raw numbers. - Skip the zero value with
_ = iotawhen 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
constblock continues counting from a previous one — each block restartsiotaat0. - Prefer leaving widely reused numeric literals (like
Pior 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
constblocks withiotato replace scattered magic numbers — it documents the whole set of related values in one place.
Practice Exercises
- Exercise 1: Define a named type
Directionwith constantsNorth,East,South, andWestusingiota, starting at0. Print all four values withfmt.Println; without aString()method, expect the raw output0 1 2 3. - Exercise 2: Using the bit-shift pattern from Example 3, define
KB,MB,GB, andTBconstants. PrintTBand check it equals1099511627776. - Exercise 3: Define file-permission-style flag constants
ReadPerm,WritePerm, andExecPermusing1 << iota(so they are1,2, and4). Combine two of them with the bitwise OR operator (|) and confirm you can test for a specific permission with bitwise AND (&).
Summary
- A
constdeclaration’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).
iotaonly has meaning inside aconst ( ... )block: it starts at0and increments by one per line, resetting to0in every new block.- A line that omits its expression implicitly repeats the previous line’s expression, re-evaluated with the new
iotavalue. iotais the idiomatic way to build enumerated constants and bit-flag sets in Go, since the language has no built-inenumkeyword.- Give an enum-like constant group its own named type and a
String()method for readable, type-safe output.
