Constants and iota, the elegant Go enum
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.
const Pi = 3.14
const Greeting = "namaste"
const MaxRetries = 3Three rules to remember:
- A
constcan only hold basic types: numbers, strings, booleans. No slices, maps, structs, time.Time, functions. - A
constcannot be the result of a function call.const now = time.Now()fails. The value must be a literal or another constant. - A
consthas no address. You cannot do&MaxRetries. They live inside the program, not in memory.
Senior rule: if you reach for
varfor something that never changes and is a number or string, preferconst. 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.
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:
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:
iotais 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.
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:
var role UserRole = 99 // ok, 99 fits in int
var n int = RoleAdmin // COMPILE ERROR: cannot use UserRole as intThat 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:
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.
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.
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.
The #1 iota interview question
Look at this code. What does it print?
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
iotaperconstblock. If you need a continuous sequence across logical groups, keep them in the sameconst ( ... ). 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.
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: adminFor 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.
Watching quietly. Tap me if you want a tip.
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
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
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)