1. iota basics: why it exists and how it counts
If you’re just starting to program, it feels like everything is simple: the “new task” status is 0, “in progress” is 1, “done” is 2. But a week goes by, you add “paused,” then someone rearranges the values, and then “flags” (checkbox-like options) show up too—and the code turns into a museum of “magic numbers.” iota is like a neat ruler: it helps you number values in a consistent way so the code reads by meaning, not like a riddle.
Imagine two versions of the code. The first is “whatever works”:
package main
import "fmt"
func main() {
const (
StatusNew = 0
StatusInProgress = 1
StatusDone = 2
)
fmt.Println(StatusDone) // 2
}
It works—until someone makes a manual mistake. The second version is “as agreed”:
package main
import "fmt"
func main() {
const (
StatusNew = iota // 0
StatusInProgress // 1
StatusDone // 2
)
fmt.Println(StatusDone) // 2
}
The details of how this works are a bit below. But the idea is the same—except now the “numbering” isn’t smeared across the code: you can clearly see it’s a sequence.
And yes: sometimes programmers argue about what’s worse—magic numbers or magic strings. The practical answer is usually this: magic numbers are worse when they also look perfectly logical.
How iota works: a line counter in a const block
It’s best to become friends with iota as if it were a simple machine: it has one rule, and it doesn’t try to be smarter than you. iota exists only inside const ( ... ), starts at 0 on the first line of the block, and increases by 1 on each next line.
Not “for each declared constant,” not “for each comma,” but specifically per line (per logical declaration line inside the block). This matters, because you’ll sometimes skip values, and then you need to understand what exactly got skipped.
Here’s a small table showing how the compiler “sees” such a block:
| Line in the const block | Expression | Value |
|---|---|---|
| 1 | |
0 |
| 2 | |
1 |
| 3 | |
2 |
Let’s verify this with code (and also practice printing values):
package main
import "fmt"
func main() {
const (
A = iota
B = iota
C = iota
)
fmt.Println(A, B, C) // 0 1 2
}
Now—an important trick, without which iota looks more boring than it should. In a const block, you can omit the expression on the right, and then Go repeats the expression from the previous line, simply substituting the new value of iota. In other words, the compiler is like: “OK, you didn’t write a formula—I’ll repeat the same one.”
package main
import "fmt"
func main() {
const (
A = iota
B
C
)
fmt.Println(A, B, C) // 0 1 2
}
One more nuance: iota resets to 0 in every new const block. This is convenient: you can have separate independent numberings (statuses, levels, command codes) and not worry that they will “continue counting” from each other.
2. Enum and flags: two different meanings
Enumerations: one value from a set
An enumeration in Go is most often done like this: you create a meaningful numeric type (type Status int) and a set of constants of that type. The effect is nice: you still have a number (easy to store and compare), but the code reads that number as a “clear name.”
And the compiler won’t let you accidentally mix “status” and, say, “age,” because those are different types.
Let’s make a small enum for a task status in a learning mini-app:
package main
import "fmt"
type TaskStatus int
const (
StatusNew TaskStatus = iota
StatusInProgress
StatusDone
)
func main() {
var s TaskStatus = StatusInProgress
fmt.Println(s) // 1
}
There are two important things here. First, TaskStatus is a new type; it is not equal to int. Second, we explicitly typed the constants as TaskStatus, which makes checks readable: s == StatusDone looks like a normal sentence.
Now let’s add human-readable text via switch:
package main
import "fmt"
type TaskStatus int
const (
StatusNew TaskStatus = iota
StatusInProgress
StatusDone
)
func main() {
var s TaskStatus = StatusDone
switch s {
case StatusNew:
fmt.Println("status: new") // status: new
case StatusInProgress:
fmt.Println("status: in progress")
case StatusDone:
fmt.Println("status: done") // status: done
default:
fmt.Println("status: unknown")
}
}
Notice default: even if you’re “sure this value can’t happen,” life loves proving otherwise. Sometimes it’s bad user input, sometimes it’s old data, sometimes you simply forgot to update one place in the code. default is a safety cushion: not the softest one, but better than face-planting at runtime.
Flag sets: several independent features
Enum answers the question “which single option is selected?”: a task status can’t be both new and done at the same time. But sometimes you need to store a set of independent properties.
For example, a task may have options: “urgent,” “with notifications,” “archived,” “read-only”—and these properties can be combined in any way.
For this, bit flags are often used: one number where each bit is a separate “checkbox.” Today our goal is to understand how iota helps you create such flags neatly.
The most common template looks like this: 1 << iota. It means “take the number 1 and shift the single set bit by iota positions.” As a result, you get powers of two: 1, 2, 4, 8, 16…—exactly what you need for flags.
package main
import "fmt"
type TaskFlag uint
const (
FlagUrgent TaskFlag = 1 << iota
FlagNotify
FlagArchived
)
func main() {
fmt.Println(FlagUrgent, FlagNotify, FlagArchived) // 1 2 4
}
Why uint and not int? Because flags are logically a “bit mask,” not a “number that can be negative.” With uint it’s easier to reason once you start printing the binary representation and checking bits: fewer surprises, more peace of mind.
Enum and flags: how not to mix them up
They look similar: constants here, constants there, iota here, iota there. But the meaning is fundamentally different.
With enum, values go consecutively: 0, 1, 2, 3… and are compared using ==. With flags, values must be powers of two: 1, 2, 4, 8… so you can combine them in one number without “conflicts.”
Look at this table—it’s a quick way to check whether you’re “in enum mode” or “in flags mode”:
| Pattern | What the values look like | What the variable means |
|---|---|---|
Enum () |
0, 1, 2, 3… | exactly one state |
Flags () |
1, 2, 4, 8… | a set of features |
The most typical beginner mistake is to make “flags” like an enum:
// BAD for flags: 1, 2, 3 are not powers of two
const (
FlagUrgent = iota + 1 // 1
FlagNotify // 2
FlagArchived // 3 (this is where the pain starts)
)
Why is this painful? Because 3 in binary is 11, meaning “two flags are enabled at once.” So one of your “flags” actually means two flags. That’s like making a “Turn on the lights” button and unexpectedly also turning on the microwave.
3. Mini-example: reading status and flags from input
Now we’ll put everything together in a very small “application slice”: the program reads two numbers from stdin. The first is the task status code (0/1/2), the second is the flags mask code (for example, 3 means “urgent + notify” if the flags are 1 and 2).
We intentionally keep this simple: no functions, structs, or collections, so the focus stays on iota and constants.
package main
import "fmt"
type TaskStatus int
type TaskFlag uint
const (
StatusNew TaskStatus = iota
StatusInProgress
StatusDone
)
const (
FlagUrgent TaskFlag = 1 << iota
FlagNotify
FlagArchived
)
func main() {
var statusCode int
var flagsCode uint
fmt.Scan(&statusCode, &flagsCode)
status := TaskStatus(statusCode)
flags := TaskFlag(flagsCode)
switch status {
case StatusNew:
fmt.Println("status: new")
case StatusInProgress:
fmt.Println("status: in progress")
case StatusDone:
fmt.Println("status: done")
default:
fmt.Println("status: unknown")
}
// For now, just remember the flag-check template:
// "if the needed bit is set, the & result will not be 0"
if flags&FlagUrgent != 0 {
fmt.Println("flag: urgent")
}
if flags&FlagNotify != 0 {
fmt.Println("flag: notify")
}
if flags&FlagArchived != 0 {
fmt.Println("flag: archived")
}
}
There’s an important idea here: iota helped us generate the correct constants, while working with masks (operators &, |, clearing flags) is already the “second part of the story,” where we’ll study bitwise operations more systematically.
But you can memorize the basic flag-check pattern right now: flags&SomeFlag != 0.
4. iota techniques: gaps and starting from non-zero
Sometimes you want 0 to mean “unknown” or “not set,” and real values to start at 1. That’s a normal idea, especially when data comes from outside: 0 is often used as an “empty default value.”
With iota, this is done neatly: either you set the first element explicitly, or you skip the first line using _.
Here’s a variant with an explicit first value:
package main
import "fmt"
type Level int
const (
LevelUnknown Level = 0
LevelLow Level = iota // iota will be 1 here because this is the second line
LevelHigh // 2
)
func main() {
fmt.Println(LevelUnknown, LevelLow, LevelHigh) // 0 1 2
}
And here’s a variant that “skips a value” via _:
package main
import "fmt"
const (
_ = iota // skipped 0
One // 1
Two // 2
)
func main() {
fmt.Println(One, Two) // 1 2
}
Why is _ useful? Because it’s an honest signal to the reader: “yes, this is an intentional gap; it’s part of the design.” Much better than the mysterious “why does numbering suddenly start at 1, and where is 0?”
5. Common mistakes when working with iota
Mistake #1: expecting that iota works outside a const-block.
iota is not a variable and not a function. It’s a special compiler identifier that exists only inside const ( ... ). Trying to use iota in a var or in a regular expression in main will end in a compilation error. If you need a counter at runtime—that’s a regular variable and a loop.
Mistake #2: thinking that iota keeps counting across different const-blocks.
Each const block is a separate little universe, and inside it iota starts at 0. That’s convenient, but if you expected “global numbering” across the whole file, you’ll unexpectedly get identical values in different groups of constants. When you truly need global numbering, it’s usually better to rethink the design: groups should typically be independent.
Mistake #3: rearranging lines and accidentally changing values.
iota numbers “in order.” That’s great when order really matters, and terrible when the order is accidental. If you insert a value “in the middle,” all subsequent ones shift. For enums this is sometimes acceptable, but for external protocols (for example, if the numbers are already stored somewhere) it’s dangerous. In such cases, it’s better to fix values explicitly and use iota carefully.
Mistake #4: creating flags with iota, but forgetting about 1 << iota.
For flags, it’s not enough to have “0, 1, 2, 3.” You need powers of two, otherwise combinations will start overlapping. If you see “flags” declared as FlagX = iota, that’s almost always a red flag: the correct form is FlagX = 1 << iota.
Mistake #5: mixing enum and flags in one variable and checking them the same way.
Enums are checked with ==, because it’s “one value from a set.” Flags are checked via a mask, because it’s “a set of features.” When you catch yourself thinking “why not make the status flags too?”—stop and ask: “can a task be both done and in progress at the same time?” If not, it’s an enum. If yes, maybe it’s a different domain already and you should separate the concepts.
GO TO FULL VERSION