What Is iota in Go and How to Use It for Enums

iota is a predeclared constant that auto-increments in const blocks to easily define sequential integer values for enums.

The config parser problem

You are writing a parser for a configuration file. The file can be in JSON, YAML, or TOML format. You need a way to represent these formats in your code. You could use strings, but strings are slow to compare and prone to typos. You could use integers, but typing const JSON = 0, const YAML = 1, const TOML = 2 is tedious. What happens when you add a new format in the middle? You have to renumber everything manually.

Go solves this with iota. It is a predeclared identifier that acts as a line counter inside constant blocks. You define a sequence of constants, assign iota to the first one, and the compiler fills in the rest. You get unique, sequential values without typing numbers.

How iota works

iota is a constant generator. Inside a const block, iota starts at zero and increments by one for every constant definition. It resets to zero whenever a new const block begins. You don't type iota as a value; you use it as a placeholder that the compiler replaces with the correct integer during compilation.

The compiler processes iota at compile time. It scans the block, assigns 0 to the first line, 1 to the second, and so on. By the time the binary runs, iota is gone. The code contains only the integer values. This means zero runtime cost. You get the convenience of auto-numbering without any performance penalty.

iota is an untyped constant. Untyped constants in Go have no specific type until they are used. They carry a default type, usually int, but they can be assigned to any compatible type. This flexibility allows iota to work with custom types like LogLevel or Permission. When you assign iota to a typed constant, the compiler checks that the value fits the type and performs the conversion.

Here's the basic pattern: define a type, open a const block, assign iota to the first item, and let the compiler fill in the rest.

package main

import "fmt"

// LogLevel defines a custom type for log severity.
// Using a custom type prevents accidental comparison with plain ints.
type LogLevel int

const (
	// Debug gets value 0. iota starts at 0 here.
	Debug LogLevel = iota
	// Info gets value 1. iota increments automatically.
	Info
	// Warn gets value 2.
	Warn
	// Error gets value 3.
	Error
)

func main() {
	// Prints 0 1 2 3.
	fmt.Println(Debug, Info, Warn, Error)
}

The compiler aligns the types and values. gofmt handles the formatting, so you don't need to worry about indentation. Most editors run gofmt on save, keeping your code consistent.

iota is a counter, not a variable. It exists only during compilation.

Realistic patterns

Real code often needs more than sequential numbers. Bitmasks are a common use case where iota shines. When you need to represent a set of options, like file permissions, you use bits. Each permission occupies one bit. You can combine permissions using bitwise OR and check them using bitwise AND. iota makes this easy because you can shift 1 by iota.

Here's how bit flags work with iota. The expression 1 << iota shifts the number 1 left by the current value of iota. This produces powers of two: 1, 2, 4, 8, and so on. Each value has exactly one bit set.

package main

import "fmt"

// Permission defines bit flags for file access control.
// Bit flags allow combining multiple permissions into a single integer.
type Permission int

const (
	// Read gets 1 << 0, which is 1. iota is 0 here.
	Read Permission = 1 << iota
	// Write gets 1 << 1, which is 2. iota is 1 here.
	Write
	// Execute gets 1 << 2, which is 4. iota is 2 here.
	Execute
)

func main() {
	// Combine Read and Write using bitwise OR.
	// Result is 3 (binary 11), meaning both bits are set.
	rw := Read | Write
	// Print binary representation to see the flags clearly.
	fmt.Printf("Read: %b, Write: %b, Execute: %b, RW: %b\n", Read, Write, Execute, rw)
}

Another pattern is the zero value. The zero value of a type is the value you get when you declare a variable without initialization. var level LogLevel creates a variable with value 0. Since iota starts at 0, the first constant in your block becomes the zero value. This is a powerful convention. You should always put the default or "unknown" state first. If your logger starts with level 0, it should probably be Debug or Info, not Error.

You can also implement the Stringer interface to get human-readable names. This is useful for logging and debugging. The String method returns a string representation of the value.

package main

import "fmt"

// LogLevel defines a custom type for log severity.
type LogLevel int

const (
	// Debug is the zero value and default level.
	Debug LogLevel = iota
	// Info is for general messages.
	Info
	// Warn indicates potential issues.
	Warn
	// Error marks failures.
	Error
)

// String returns the name of the log level.
// This implements fmt.Stringer for pretty printing.
func (l LogLevel) String() string {
	switch l {
	case Debug:
		return "DEBUG"
	case Info:
		return "INFO"
	case Warn:
		return "WARN"
	case Error:
		return "ERROR"
	default:
		return fmt.Sprintf("LogLevel(%d)", l)
	}
}

func main() {
	var level LogLevel
	// level is 0, which is Debug.
	fmt.Println("Default level:", level)
	level = Error
	fmt.Println("Current level:", level)
}

The zero value is a feature. Design your enums around it.

Pitfalls and errors

iota increments for every line, even if you leave a line blank or put a comment. If you skip a constant, the counter still moves. This can create gaps in your values.

const (
	First = iota
	Second
	// Third is omitted intentionally.
	// iota still increments here.
	Fourth = iota // Fourth is 3, not 2.
)

iota resets for every const block. If you have two blocks, iota starts over. Don't assume continuity across blocks.

const (
	A = iota // A is 0.
	B        // B is 1.
)

const (
	C = iota // C is 0 again. iota resets.
	D        // D is 1.
)

You can only use iota inside constant declarations. If you try to use it in a var block or a function, the compiler rejects it.

The compiler rejects iota outside a constant declaration with iota is not a constant.

Type safety prevents mixing constants from different types. If you define LogLevel and Permission as separate types, you can't assign a LogLevel to a Permission variable, even if both are based on int.

The compiler rejects mixing types with cannot use Debug (type LogLevel) as type Permission in assignment.

This type safety catches bugs where you accidentally compare a log level to a permission flag. In C, enums are just integers. You can compare an enum to any integer and the compiler won't complain. Go's approach catches these mistakes early.

iota counts lines, not constants. Watch the gaps.

When to use iota

Use iota when you need a sequence of unique integer values and the actual numbers don't matter. Use iota with bit shifts when you need a set of flags that can be combined with bitwise operations. Use explicit integers when the values have semantic meaning, like HTTP status codes or magic numbers from a protocol spec. Use strings when the values need to be human-readable in logs or APIs without conversion. Use iota with expressions when you need a pattern like powers of two or scaled values.

Pick the tool that matches the semantics. Numbers for flags, strings for display, iota for sequences.

Where to go next