ToolPopToolPop
Go · Lesson 4 of 22

Constants and iota, the elegant Go enum

7 min readUpdated 25 Jun 2026

If you came from JavaScript, "constant" might feel boring. In Go, const is a different beast. It is checked at compile time, it cannot hold most kinds of values, and it gives you iota, which is the most elegant way to write enums in any language I have used.

Let us start from the most basic constant and work up to the interview pattern that confuses everyone.

What const actually is

A constant in Go is a value that is fixed at compile time. The compiler bakes it directly into your binary. There is no "runtime constant variable" idea like in JavaScript.

go
const Pi = 3.14
const Greeting = "namaste"
const MaxRetries = 3

Three rules to remember:

  • A const can only hold basic types: numbers, strings, booleans. No slices, maps, structs, time.Time, functions.
  • A const cannot be the result of a function call. const now = time.Now() fails. The value must be a literal or another constant.
  • A const has no address. You cannot do &MaxRetries. They live inside the program, not in memory.

Senior rule: if you reach for var for something that never changes and is a number or string, prefer const. The compiler catches mistakes you would otherwise find in production.

iota: the auto-incrementing counter

iota is a special built-in identifier you can use only inside a const ( ... ) block. It starts at 0 on the first line and increases by 1 for every subsequent line in the same block.

go
const (
    Sunday    = iota // 0
    Monday    = iota // 1
    Tuesday   = iota // 2
    Wednesday = iota // 3
)

If you want the same expression on every line, Go lets you skip repeating it:

go
const (
    Sunday = iota // 0  (the formula = iota is set here)
    Monday        // 1  (Go repeats the previous formula, iota is now 1)
    Tuesday       // 2
    Wednesday     // 3
)

This shorter form is what you will see in every real Go codebase. The repeated = iota is just for teaching.

Interview trap: iota is line-number-like, not declaration-number-like. Blank lines and comments do not increment it. Only declarations do.

Pattern 1: type-safe enums

This is the most common real use of iota. You declare a custom type AND build the enum at the same time.

go
package main
 
import "fmt"
 
type UserRole int  // a custom integer type
 
const (
    RoleGuest     UserRole = iota // 0
    RoleMember                    // 1
    RoleAdmin                     // 2
    RoleModerator                 // 3
)
 
func main() {
    var myRole UserRole = RoleAdmin
    fmt.Println(myRole) // 2
}

Why declare type UserRole int instead of just using int?

Because then RoleAdmin is of type UserRole, not just any int. The compiler will refuse:

go
var role UserRole = 99   // ok, 99 fits in int
var n int = RoleAdmin    // COMPILE ERROR: cannot use UserRole as int

That second line failing is a feature. You cannot accidentally assign a database ID (int) into a role field (UserRole). The type system catches it.

Pattern 2: skip the zero value

The zero value of UserRole is 0 = RoleGuest. That might be a problem. If you forget to set a role field, the user accidentally becomes a guest.

The fix is to throw away the zero value:

go
const (
    _            UserRole = iota // 0 skipped (blank identifier)
    RoleMember                   // 1
    RoleAdmin                    // 2
    RoleModerator                // 3
)

Now an uninitialised UserRole is 0, which matches NO valid role. You can write a Validate() method that flags zero as invalid.

This is the canonical "no accidental default" pattern. Use it whenever the zero value would silently mean something.

Pattern 3: bit flags with shifts

When you have several boolean attributes (read, write, execute) and want to combine them, use iota with << (left shift) to make each constant a single bit.

go
type Permission uint8
 
const (
    Read    Permission = 1 << iota // 1 << 0 = 1   (binary: 00000001)
    Write                          // 1 << 1 = 2   (binary: 00000010)
    Execute                        // 1 << 2 = 4   (binary: 00000100)
)
 
func main() {
    var p Permission = Read | Write   // combine flags with bitwise OR
    fmt.Println(p)                     // 3 (binary 00000011)
    fmt.Println(p & Read != 0)         // true (test if Read bit is set)
}

This is how Linux file permissions work, how Go's regexp flags work, how many networking libraries express options. Pack many booleans into one number.

Pattern 4: custom math on iota

iota is just an integer. You can do arithmetic on it.

go
const (
    KB = 1 << (10 * (iota + 1)) // 1 << 10 = 1024
    MB                          // 1 << 20 = 1048576
    GB                          // 1 << 30
    TB                          // 1 << 40
)

Each line repeats the formula with iota bumped. So you get a clean power-of-two scale for file sizes.

Diagram
rendering diagram...
How iota progresses in one const block

The #1 iota interview question

Look at this code. What does it print?

go
const (
    A = iota
    B
)
 
const (
    C = iota
    D
)
 
func main() {
    fmt.Println(A, B, C, D)
}

The trap is in the answer most people give: "0, 1, 2, 3."

The actual output is 0 1 0 1.

Why? Because iota resets to 0 at the start of every separate const ( ... ) block. Each block has its own counter. The interviewer is testing whether you understand the scope, not the syntax.

Senior rule: one iota per const block. If you need a continuous sequence across logical groups, keep them in the same const ( ... ). If you have separate concepts, separate blocks are correct.

Pattern 5: stringer for pretty printing

By default, printing an enum gives you the underlying number. Add a String() method and fmt.Println will use it.

go
func (r UserRole) String() string {
    switch r {
    case RoleMember:    return "member"
    case RoleAdmin:     return "admin"
    case RoleModerator: return "moderator"
    default:            return "unknown"
    }
}
 
// fmt.Println(RoleAdmin) now prints: admin

For long enums, the Go team has a tool called stringer that generates this method for you. go install golang.org/x/tools/cmd/stringer@latest, then //go:generate stringer -type=UserRole at the top of the file.

iota essentials

const
Compile-time constant. Only basic types (numbers, strings, booleans). No address, no function-call values.
iota
Built-in identifier usable only inside a const ( ... ) block. Starts at 0 on the first line, increments by 1 each subsequent line. Resets per block.
blank identifier (_)
A throwaway slot. Use it as a constant name when you want to skip a value (commonly to throw away the 0).
bit flag
An integer where each bit represents a boolean attribute. Combine with |, test with &.
stringer
Tool by the Go team that generates a String() method for any int-based enum type. Saves you the boilerplate switch.

When NOT to use iota

  • When the numeric values must match an external system (HTTP status codes, error codes from a vendor API). Hardcode them explicitly.
  • When the constants are not really related (don't lump them together just to share iota).
  • When the order matters semantically and might change in the future (someone adding a value in the middle silently renumbers everything below it).

Senior rule: iota is great for closed sets you control end-to-end. For values that cross a network or persist to a database, write the numbers explicitly. Future-you will thank you when refactoring.

That is iota. Five patterns and one trap. You are now equipped to read 80% of the enum code in any Go codebase.

Chai0/2 done

Watching quietly. Tap me if you want a tip.

Go Playground

Go cannot run in the browser. Run opens go.dev/play with your code already loaded (one click, no paste).

Try this (0 of 2 done)

  1. 1

    Predict the output. (Hint: iota resets per const block.)

    hint

    Each const ( ... ) block has its own iota counter starting at 0.

    show answer
    // editor has it
  2. 2

    Predict: with `const ( _ = iota; KB = 1 << (10 * iota) )`, what is KB?

    hint

    iota is 1 on the KB line. 1 << 10 = 1024.

    show answer
    const (
    	_  = iota
    	KB = 1 << (10 * iota)
    )
    fmt.Println(KB)