Constants and iota in Go With Patterns Worth Stealing
How iota numbers a const block, why a bare line repeats the one above, and five patterns: enums, bit flags, byte sizes, name tables and compile-time checks.

Almost every Go program grows a set of named options: log levels, states, permissions, units. The language has no enum keyword, so what you write is a type and a block of constants, and iota is the piece that keeps the block short. It is also the piece with the sharpest edge, because the numbers it hands out are an accident of the order you typed the lines. This guide covers the counting rule exactly, shows the five patterns I reach for, and ends with the failure mode that costs real data. It sits in the Go basics path next to typed and untyped constants, and the two are worth reading together.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
What a constant is in Go
A constant is a value the compiler knows. You declare one with const, and it can be a number, a string, a boolean or a rune. Constants cannot be the result of a function call, which is the main difference from a variable that you never reassign. time.Now() cannot initialize a constant, but 60 * 60 can, and so can len("abc").
const Pi = 3.14159
const Greeting = "hello"
const (
MaxRetries = 5
Timeout = 30
)Constants without a type are called untyped, and they carry extra precision and flexibility. The details are in the article on untyped constants. For this guide the one fact that matters is that you can attach a named type to a constant, and that is how enums get their type safety.
The counting rule, and the line that repeats itself
iota is a predeclared identifier that only means something inside a const declaration. Inside it, iota is the zero-based index of the current line (the Go specification calls it a ConstSpec) within the block. Two properties explain every example you will ever see. The specification's section on iota states both.
First, the counter resets to 0 at the start of each new const block. Second, if a line has no expression, the compiler reuses the previous line's expression and type, and iota has moved on by one. Here are the possibilities side by side:
package main
import "fmt"
const (
A = iota
B
C
)
const (
X = iota * 10
Y
Z
)
const (
P, Q = iota, iota * 2
R, S
)
const (
_ = iota
One
Two
_
Four
)
const (
First = iota
Second = "text"
Third
Fourth = iota
)
const Alone = iota
func main() {
fmt.Println(A, B, C)
fmt.Println(X, Y, Z)
fmt.Println(P, Q, R, S)
fmt.Println(One, Two, Four)
fmt.Println(First, Second, Third, Fourth)
fmt.Println(Alone)
}0 1 2
0 10 20
0 0 1 2
1 2 4
0 text text 3
0Read each group against the rule. A, B, C are 0 1 2 because the second and third lines repeat iota and the counter advances. X, Y, Z repeat iota * 10, so they are 0 10 20. The P, Q pair shows two names on one line, both using the same iota value, and the next line repeats the whole pair. The blank identifier _ consumes a number without creating a name, which is how you skip zero (or a gap in the middle). The First group is the one people get wrong: the line Second = "text" still counts as line 1, Third repeats the string, and Fourth shows that iota is now 3, not 2 or 1. A line spends a number even when it does not use it.
The last declaration, Alone, is a block of one. Its iota is 0, because a standalone const is its own block. That is useful to know when someone writes iota outside parentheses and wonders why it never counts.
Pattern 1: a typed enum with a readable name
Go has no enum type, so you build one in three parts: a named type with an integer underlying type, a block of constants of that type, and a String method so the values print as words. Nothing else has to be built in, because fmt calls String when it exists.
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
var dayNames = [...]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
func (d Weekday) String() string {
if d < 0 || int(d) >= len(dayNames) {
return fmt.Sprintf("Weekday(%d)", int(d))
}
return dayNames[d]
}
func main() {
fmt.Println(Monday, Weekday(9))
fmt.Printf("%d %v %s\n", Monday, Monday, Monday)
}Monday Weekday(9)
1 Monday MondayThe typed first line, Sunday Weekday = iota, is what carries the type to every line below it. Monday is a Weekday, not an untyped 1, so a function that takes a Weekday will not accept a bare int variable without a conversion. That is a real safety gain: you cannot pass a port number where a day is expected by accident. It is not airtight. A constant like Monday + 20 is still a legal Weekday, and Weekday(9) compiles, which is why the String method checks its range instead of indexing blindly. The out-of-range value prints as Weekday(9) instead of panicking in a log line.
The String method guards against a panic that would otherwise occur only in production, on an input nobody tested. I always write the range check, even when I believe the value is already validated. There is more on the same idea, including a way to generate the method, in the guide to enums done properly and the Stringer tool.
Typed or untyped: what the type buys you
Whether the first line carries a type decides how the constants behave everywhere they are used. Without one, iota produces untyped integer constants, which convert silently into any numeric type that can hold them. With one, every constant in the block has that type and the compiler enforces it:
package main
import "fmt"
const (
Loose = iota
)
type Strict int
const One Strict = 1
func main() {
var f float64 = Loose + 0.5
var n int32 = Loose + 3
fmt.Println(f, n)
m := int(One)
fmt.Println(m, One)
}0.5 3
1 1The untyped Loose happily becomes a float64 and an int32. That flexibility is right for a counter, an array size or a unit multiplier. It is wrong for an enum, because a function that takes a log level should not accept a port number. Assign the typed constant to a plain int and the compiler refuses:
package main
type Strict int
const One Strict = 1
func main() {
var m int = One
_ = m
}# example.test/lab
./main.go:8:14: cannot use One (constant 1 of int type Strict) as int value in variable declarationThe message names both types, which is the whole point of declaring one. I give every enum a named type, even when it is only two or three values, and keep untyped iota for things that are plainly numbers.
Pattern 2: reserve zero for unknown
The zero value of an integer type is 0, and a struct field that nobody set is 0. If 0 happens to be your first real option, an unset field silently means "the first option", and nobody notices until the first option is something expensive. The usual defence is to make zero mean nothing:
package main
import "fmt"
type Status int
const (
StatusUnknown Status = iota
StatusActive
StatusSuspended
statusCount
)
func (s Status) Valid() bool {
return s > StatusUnknown && s < statusCount
}
type Account struct {
Name string
Status Status
}
func main() {
var a Account
a.Name = "demo"
fmt.Println(a.Status, a.Status.Valid())
a.Status = StatusActive
fmt.Println(a.Status, a.Status.Valid())
fmt.Println(Status(7).Valid())
}0 false
1 true
falseA fresh Account reports Status zero, and Valid() returns false, so the code that validates input before saving rejects it. The unexported statusCount at the end is a sentinel: it is never a real value, and it marks where the valid range stops, so Valid does not need editing when you add a new status above it. This is the same instinct as the rule in designing types around a useful zero value, applied to enums.
The counter-argument is real. Some enums do have a natural default, and StatusUnknown is noise in a type where every value is meaningful. A Direction with North, East, South and West has no "unknown". If you can show that an unset value is impossible, skipping the sentinel is fine. I include it whenever the value will be stored, parsed from input or decoded from JSON, because those are the paths that produce zero by accident.
Pattern 3: bit flags with 1 shifted left
When several options can be on at once, give each one its own bit. Writing the shift with iota is the standard way:
package main
import "fmt"
type Perm uint8
const (
Read Perm = 1 << iota
Write
Exec
)
func (p Perm) String() string {
out := []byte("---")
if p&Read != 0 {
out[0] = 'r'
}
if p&Write != 0 {
out[1] = 'w'
}
if p&Exec != 0 {
out[2] = 'x'
}
return string(out)
}
func main() {
p := Read | Write
fmt.Println(p, p&Exec != 0)
p |= Exec
fmt.Println(p)
p &^= Write
fmt.Println(p, uint8(p))
}rw- false
rwx
r-x 5Read is 1 << 0, Write is 1 << 1 and Exec is 1 << 2, so the values are 1, 2 and 4. Combine flags with |, test with &, and clear one with &^ (the "and not" operator, a Go original). The output shows the three operations and the final raw value. If you want to see the bits for a different value, the bitwise calculator is a quick check, and bit manipulation in Go goes through the operators one by one.
Pick the width deliberately. A uint8 holds 8 flags, a uint32 holds 32 and a uint64 holds 64. Declare a ninth flag on a uint8 and the compile fails with a constant overflow error, which is the check you want. Do not use a signed type for flags: shifting into the sign bit produces a negative number that prints oddly and compares unexpectedly.
Pattern 4: sizes and units from one expression
Because the arithmetic is done by the compiler with arbitrary precision, iota can drive unit tables that would be tiresome to write by hand. This one is close to the example in Effective Go:
package main
import "fmt"
type ByteSize float64
const (
_ = iota
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
)
func (b ByteSize) String() string {
switch {
case b >= TB:
return fmt.Sprintf("%.2f TB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2f GB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2f MB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2f KB", b/KB)
}
return fmt.Sprintf("%.0f B", b)
}
func main() {
fmt.Println(ByteSize(512), KB, 1.5*MB, ByteSize(3_221_225_472), 12*TB)
}512 B 1.00 KB 1.50 MB 3.00 GB 12.00 TBThe _ = iota on the first line burns zero, so KB gets iota equal to 1 and its value is 1 << 10. MB repeats the expression with iota equal to 2, giving 1 << 20, and so on. These are binary units (KiB and so on, strictly), and I kept the common names to match what most tools print. If your output is read by other people, decide whether you mean powers of 1,000 or 1,024 and say it, as a disk vendor and an operating system will disagree on what a gigabyte is. A quick conversion check is the byte converter tool.
Starting somewhere other than zero, and leaving gaps
iota always starts at zero, but the expression around it is free. Adding an offset moves the start, and multiplying spaces the values out. Spacing is the trick I like most, because it leaves room to insert a value later without renumbering anything:
package main
import "fmt"
type Priority int
const (
Low Priority = (iota + 1) * 10
Normal
High
Urgent
)
const (
StatusBase = 100 + iota
StatusA
StatusB
)
func main() {
fmt.Println(Low, Normal, High, Urgent)
fmt.Println(StatusBase, StatusA, StatusB)
}10 20 30 40
100 101 102If a fifth priority has to fit between Normal and High later, you give it 25 and nothing else moves. With plain 0, 1, 2, 3 there is no room, and you would renumber or append out of order. The cost is that the numbers carry no meaning by themselves, so any code that compares priorities must use the named constants and the ordering, never the raw numbers. For values that must match someone else's table, such as numbers from a protocol, offsetting iota is still guessing at a contract. Write the numbers out.
Pattern 5: a name table the compiler checks
Names for enum values are usually a slice or array indexed by the constant, which has one flaw: add a constant, forget the name, and the table is one short. Index out of range happens at runtime, in whatever code path prints the new value first. A constant expression catches it at compile time:
package main
import "fmt"
type Color int
const (
Red Color = iota
Green
Blue
numColors
)
var colorNames = [...]string{Red: "red", Green: "green", Blue: "blue"}
// Fails to compile if a Color is added without a name.
const _ = uint(len(colorNames) - int(numColors))
func (c Color) String() string { return colorNames[c] }
func main() {
fmt.Println(Red, Blue, len(colorNames))
}red blue 3The array literal uses keyed elements, so each name sits beside its constant. len(colorNames) is itself a constant because the array has a fixed size, and numColors counts the values. The difference is 0 when they agree. Now add Purple before numColors and forget its name:
package main
import "fmt"
type Color int
const (
Red Color = iota
Green
Blue
Purple
numColors
)
var colorNames = [...]string{Red: "red", Green: "green", Blue: "blue"}
const _ = uint(len(colorNames) - int(numColors))
func (c Color) String() string { return colorNames[c] }
func main() {
fmt.Println(Red, Blue, len(colorNames))
}# example.test/lab
./main.go:17:16: constant -1 overflows uintThe negative result cannot become a uint, so the compiler stops and points at the constant line. It is a small trick, and it turns "someone will forget" into a build failure. A linter such as exhaustive covers the switch side of the same problem, flagging a switch over an enum that misses a case.
How the standard library counts
Reading real declarations is the quickest way to see which patterns survive contact with a large codebase. The standard library uses all of the shapes above, and you can print them to check your understanding of the rule:
package main
import (
"fmt"
"log"
"os"
"reflect"
"time"
)
func main() {
fmt.Println(log.Ldate, log.Ltime, log.Lmicroseconds, log.Llongfile, log.Lshortfile, log.LUTC, log.Lmsgprefix, log.LstdFlags)
fmt.Println(int(time.January), int(time.December), time.Saturday == 6)
fmt.Println(reflect.Invalid, reflect.Bool, int(reflect.Int), reflect.String)
fmt.Printf("%032b\n", uint32(os.ModeDir))
}1 2 4 8 16 32 64 3
1 12 true
invalid bool 2 string
10000000000000000000000000000000The log flags are bit flags built with 1 << iota, and LstdFlags is declared as Ldate | Ltime, so it prints 3. The months in time start at 1 because the declaration is January Month = 1 + iota, a deliberate offset so that the number matches the calendar. The days start at 0 for Sunday. reflect.Kind is a plain sequence with Invalid as zero, which is the unknown-first pattern from earlier, and the String method on Kind is why the names print instead of numbers. The file mode constant counts down from the top bit of a 32-bit value, which is a reminder that iota is only a counter and the expression decides what it means.
Notice that none of these were designed to be reordered. The time.Month numbers are used by every program that formats a date, and the declaration in the source code is the entire stability guarantee. When your own constants get the same kind of use, treat the declaration order with the same respect.
The failure that costs data: persisted iota
Everything above assumes the numbers stay inside one running program. The moment a value leaves, in a database column, a JSON field or a protocol message, the number becomes a contract. iota assigns numbers by position, so inserting a constant in the middle of the block renumbers everything after it. Code that was compiled before the change and data written before the change now disagree about what 2 means.
The standard defences, in the order I prefer them:
- Store a name, not a number. A
MarshalTextmethod makesencoding/jsonand many other encoders write the name, and anUnmarshalTextmethod reads it back. The names are the contract and the numbers can change freely. - If the number must be stored, write it out.
LevelWarn Level = 20is not a style failure; it is a promise that survives a refactor. - If you keep
iotaand must store numbers, add a comment that says "append only" and a test that pins the values.
Here is the first option, with the failure shown for an unknown value:
package main
import (
"encoding/json"
"fmt"
)
type Level int
const (
Debug Level = iota
Info
Warn
Error
)
var levelNames = [...]string{"debug", "info", "warn", "error"}
func (l Level) MarshalText() ([]byte, error) {
if l < 0 || int(l) >= len(levelNames) {
return nil, fmt.Errorf("invalid level %d", int(l))
}
return []byte(levelNames[l]), nil
}
func (l *Level) UnmarshalText(b []byte) error {
for i, name := range levelNames {
if name == string(b) {
*l = Level(i)
return nil
}
}
return fmt.Errorf("unknown level %q", b)
}
type Entry struct {
Level Level `json:"level"`
Msg string `json:"msg"`
}
func main() {
out, err := json.Marshal(Entry{Level: Warn, Msg: "disk almost full"})
fmt.Println(string(out), err)
var back Entry
err = json.Unmarshal(out, &back)
fmt.Println(back.Level == Warn, err)
err = json.Unmarshal([]byte(`{"level":"fatal","msg":"x"}`), &back)
fmt.Println(err)
}{"level":"warn","msg":"disk almost full"} <nil>
true <nil>
unknown level "fatal"The JSON says "warn", so the stored data does not care that Warn is 2 today and might be 3 after someone inserts Notice. A rejected unknown level returns an error instead of a silent zero. The cost is a few lines per enum, and the saving is the one migration nobody wants to write at midnight. For the JSON side of this, struct tags and what reads them shows how the encoder decides what to call a field.
Pin the numbers with a test
If you keep iota for a value that might get stored, make reordering loud. A test that pins each constant to its number and checks that every value has a name costs ten lines and catches the careless insertion in review:
package level
import "fmt"
type Level int
const (
Debug Level = iota
Info
Warn
Error
levelCount
)
var names = [...]string{"debug", "info", "warn", "error"}
func (l Level) String() string {
if l < 0 || l >= levelCount {
return fmt.Sprintf("Level(%d)", int(l))
}
return names[l]
}package level
import "testing"
func TestNumbersArePinned(t *testing.T) {
want := map[Level]int{Debug: 0, Info: 1, Warn: 2, Error: 3}
for level, number := range want {
if int(level) != number {
t.Errorf("%s is %d, stored data expects %d", level, int(level), number)
}
}
}
func TestEveryLevelHasAName(t *testing.T) {
for l := Debug; l < levelCount; l++ {
if l.String() == "" {
t.Errorf("level %d has no name", int(l))
}
}
if got := Level(9).String(); got != "Level(9)" {
t.Errorf("out of range level printed %q", got)
}
}=== RUN TestNumbersArePinned
--- PASS: TestNumbersArePinned (0.00s)
=== RUN TestEveryLevelHasAName
--- PASS: TestEveryLevelHasAName (0.00s)
PASS
ok example.test/lab 1.258sThe first test is the one that matters. Insert Notice between Info and Warn and it fails with a message naming the constant, the new number and the number the stored data expects. That is a review comment written by the build. The second test covers the sentinel and the out-of-range path, so the String method cannot panic.
Things iota will not do
It cannot be used outside a const declaration, so x := iota is a compile error. It does not skip on its own: gaps need _. It counts lines, not names, so grouping two constants on one line gives both of them the same number. And it only produces integers, though you can derive floats and strings from it, as the byte size example does, or index a table of strings by it. If a value needs to be parsed from text, validated against a range or round-tripped through a database, iota is only the starting point and the methods around it do the real work.
When iota is the wrong tool
If the set of values is open, where users or plugins add new ones at runtime, constants are the wrong model. A registry keyed by string is the usual answer. If each option carries data, such as a label, a price and a flag, a struct and a map are a better fit than a number and several parallel tables. And if the values come from another system's specification, such as HTTP status codes or Unix signals, copy the numbers from the specification explicitly. The HTTP status code reference lists the canonical values, and the standard library's net/http package spells them out rather than counting. Anyone who later reads StatusNotFound = 404 can confirm it against the RFC without running the compiler in their head, and that readability is the same reason I write explicit numbers for anything stored.
The Go specification covers constant declarations in full, and the Constants article on the Go blog explains why untyped constants behave the way they do, which is the background for several of the patterns above.


