Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Golang iota: Enums That Do Not Silently Break

Sean

Platform Writer

Aug 30, 2026
8 min read

iota is a counter that resets at every const block and increments once per ConstSpec line. Almost every iota bug comes from one of those two facts being forgotten.

Golang iota: Enums That Do Not Silently Break

Go has no enum keyword. What it has is iota, an untyped integer counter usable inside const declarations, plus a convention that has hardened into idiom.

The mechanics take five minutes. The part worth your attention is the failure mode: iota assigns values by position, so the meaning of your constants depends on the order of lines in a file. When those values leave the process — into a database, a queue, a wire format — that becomes a real hazard.

Table of contents

The mechanics

Inside a const block, iota starts at 0 and increments by one for each ConstSpec:

const (
	StatusPending  = iota // 0
	StatusRunning         // 1
	StatusSucceeded       // 2
	StatusFailed          // 3
)

The repetition after the first line is implicit: Go repeats the previous expression, and because that expression contains iota, it re-evaluates at each new value.

It resets in every new const block. Two blocks in the same file both start at 0, which is exactly what you want for two unrelated enums and exactly the trap if you meant them to continue.

It also increments per line, not per constant. Multiple names on one line share a value:

const (
	A, B = iota, iota + 10 // A=0, B=10
	C, D                   // C=1, D=11
)

Give the enum a type

Untyped integer constants are assignable to any numeric variable, so an untyped enum offers no protection at all — you can pass a Status where a count is expected and the compiler will not object.

Define a named type:

type Status int

const (
	StatusPending Status = iota
	StatusRunning
	StatusSucceeded
	StatusFailed
)

Now func setStatus(s Status) cannot be called with a bare int variable, and the type name appears in godoc grouping the constants together. This is a small change that catches a genuine class of mistake, and there is no reason to skip it.

Note that Go still allows an untyped literalsetStatus(7) compiles even though 7 is not a valid Status. Named types narrow the hole; they do not close it, which is what the validation function below is for.

Skipping values and the blank identifier

Use _ to discard a value, most commonly to make zero unusable:

type Priority int

const (
	_ Priority = iota // reserve 0 as invalid
	PriorityLow
	PriorityMedium
	PriorityHigh
)

This is a real design decision with two defensible answers. Reserving zero means an uninitialised Priority is detectably invalid rather than silently PriorityLow — valuable when the value arrives from JSON where a missing field decodes to zero.

The counter-argument is that Go’s zero-value idiom is a strength, and a sensible zero (StatusPending, LogLevelInfo) means struct literals need not set the field. Pick per enum. The question to ask is whether an accidental zero is a bug you want to catch or a default you want to have.

The other well-known use is expressions over iota, for byte sizes:

type ByteSize float64

const (
	_           = iota
	KB ByteSize = 1 << (10 * iota) // 1 << 10
	MB                             // 1 << 20
	GB                             // 1 << 30
)

The String method, and generating it

By default an enum prints as a number, so logs say status=2 and nobody can read them. Implement Stringer:

func (s Status) String() string {
	switch s {
	case StatusPending:
		return "pending"
	case StatusRunning:
		return "running"
	case StatusSucceeded:
		return "succeeded"
	case StatusFailed:
		return "failed"
	default:
		return fmt.Sprintf("Status(%d)", int(s))
	}
}

The default case matters. Returning "unknown" for out-of-range values throws away the information you most need when debugging; printing the number keeps it.

go generate with stringer automates this, and for enums that change often it is worth it:

//go:generate stringer -type=Status

Add the generated file to version control and re-run generation in CI with a check that the working tree is clean, or the generated names drift from the constants.

The reordering hazard, and when iota is wrong

Here is the bug worth remembering. You have four statuses persisted as integers in a database. Someone adds a new one alphabetically:

const (
	StatusCancelled Status = iota // 0 -- was StatusPending
	StatusPending                 // 1 -- was StatusRunning
	StatusRunning                 // 2 -- was StatusSucceeded
	StatusSucceeded               // 3 -- was StatusFailed
	StatusFailed                  // 4
)

Everything compiles. Every test that uses the constants symbolically passes. And every row already in the database now means something different. There is no error, no warning, and the corruption is silent until someone notices cancelled jobs in the pending queue.

Two rules that prevent it:

  1. Only append. New values go at the end of the block, never in the middle, regardless of how untidy that looks.
  2. Do not use iota for values that leave the process. If the value is written to a database, sent over the wire, or stored in a file, assign it explicitly.

For persisted enums, write the values out:

const (
	StatusPending   Status = 1
	StatusRunning   Status = 2
	StatusSucceeded Status = 3
	StatusFailed    Status = 4
)

It is less elegant and it is correct. iota is for values that live and die inside one binary; explicit numbers are for values that outlive it. Better still for stored data, use strings — "succeeded" in a database column is self-describing in a way that 3 never will be, and it survives any reordering at all.

Validating at the boundary

Since Go will accept any int literal as your named type, validate wherever values enter from outside:

func (s Status) Valid() bool {
	return s >= StatusPending && s <= StatusFailed
}

func ParseStatus(v int) (Status, error) {
	s := Status(v)
	if !s.Valid() {
		return 0, fmt.Errorf("invalid status %d", v)
	}
	return s, nil
}

The range check works only while the block is contiguous — another reason the append-only rule matters. Call ParseStatus at every boundary: JSON decoding, database scanning, query parameter parsing. Inside the process you can then trust the type.

Go services that carry this kind of state deploy on RunxBuild straight from a repository, with build logs, a live route, runtime logs and rollback to a previous deploy, and a managed Postgres or MySQL alongside for the rows those status values end up in.

How this fits the rest of the stack

iota is a positional counter, and that is its whole strength and its whole danger: elegant for values confined to one binary, hazardous for anything persisted. Give every enum a named type and a String method, append rather than insert, and write explicit values — or strings — for anything that reaches a database. The service and the database behind it are the parts with a monthly number attached, and the RunxBuild hosting calculator shows them together.

Useful related references:

FAQ

What is iota in Go?

A predeclared identifier usable in const declarations that starts at 0 in each const block and increments by one per ConstSpec line. It gives Go a concise way to declare sequential constants in the absence of an enum keyword.

Does iota reset between const blocks?

Yes. Every new const block starts iota at 0 again. That is correct for unrelated enums and a common surprise for anyone expecting a second block to continue the first one’s numbering.

Should I start an enum at 0 or 1?

It depends on whether an accidental zero is a bug you want to catch. Reserving 0 with the blank identifier makes an uninitialised value detectably invalid, which helps when values arrive from JSON. A meaningful zero keeps Go’s zero-value idiom working in struct literals.

Is it safe to store iota values in a database?

No. iota assigns by position, so inserting a constant in the middle of the block silently changes the meaning of every stored row, with no compile error. Use explicit numeric values for persisted enums, or better, store strings.

How do I print an enum name instead of a number?

Implement String() string on the named type, either by hand with a switch or by generating it with stringer via a //go:generate directive. Have the default case print the underlying number so unexpected values stay debuggable.

#golang iota#Go enums#Go constants#Go types#Go patterns