Constants & iota
Reviewed & published by Brayan K
A constant is a value fixed at compile time that can never change, and iota is Go's built-in counter for generating sequences of related constants like enums and bit flags.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Declare constants with const, singly and in blocks
- Tell typed from untyped constants and why it matters
- Use iota to number enum-style constants
- Skip and shift values with the blank identifier
- Build powerful bit flags with 1 << iota
- Combine and test flags with | and &
1️⃣ Declaring Constants
You declare a constant with the const keyword. Because the value is fixed at compile time, the compiler can compute expressions like 60 * 60 * 24 up front — but it will reject anything it can't evaluate, like a function-call result. You can declare constants one at a time or group related ones in a const ( ... ) block. Reassigning a constant is a compile error, which makes constants a great way to lock down values that must never drift.
package main
import "fmt"
func main() {
// A constant is a value fixed at compile time — it can never change.
const Pi = 3.14159
const Greeting = "hello"
// Constants can be declared in a block, like a list.
const (
MaxUsers = 100
AppName = "LearnGo"
)
fmt.Println(Pi, Greeting)
fmt.Println(MaxUsers, AppName)
// Trying to change a constant is a COMPILE error, so we can't even show it.
// Pi = 3.2 // ❌ cannot assign to Pi
// Constants are evaluated at compile time, so they can build on each other.
const SecondsPerDay = 60 * 60 * 24
fmt.Println("seconds in a day:", SecondsPerDay)
}2️⃣ Typed vs Untyped Constants
An untyped constant (like const big = 1 << 40) has no fixed type until you actually use it — it adapts to whatever context it lands in, so the same constant can become a float64 in one place and an int64 in another. A typed constant (const maxRetries int = 5) is locked to one type, so mixing it with a different type needs an explicit conversion. Leaving constants untyped is usually the more flexible choice.
package main
import "fmt"
func main() {
// An UNTYPED constant has no fixed type until it's used. It adapts.
const big = 1 << 40 // way bigger than an int32, but fine as untyped
var f float64 = big // becomes a float64 here
var i int64 = big // and an int64 here — same constant, two types
fmt.Println(f, i)
// Untyped constants keep high precision and fit wherever they're valid.
const third = 1.0 / 3.0
fmt.Printf("%.10f\n", third)
// A TYPED constant is locked to one type.
const maxRetries int = 5
var attempts int = maxRetries // ok: both int
fmt.Println(attempts)
// Mixing a typed constant with another type needs a conversion:
var ratio float64 = float64(maxRetries) / 2
fmt.Println(ratio)
}3️⃣ iota: The Enumerator
iota is a built-in counter that resets to 0 at the start of every const block and increases by 1 for each line — whether or not iota appears on that line. That makes it perfect for enum-style constants like the days of the week. You can skip a value by assigning a line to the blank identifier _, and you can shift the whole sequence with arithmetic such as 1 << (10 * iota) to build a kilobyte / megabyte ladder.
package main
import "fmt"
// iota is a counter that resets to 0 in each const block and
// increments by 1 for every line. It's Go's enum builder.
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func main() {
fmt.Println(Sunday, Wednesday, Saturday) // 0 3 6
// You can start from a different value and skip with the blank _.
const (
_ = iota // skip 0
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)
fmt.Println(KB, MB, GB)
}4️⃣ Bit Flags with iota
Shifting 1 << iota gives you 1, 2, 4, 8, ... — each a single distinct bit. Because no two flags share a bit, you can pack several on/off options into one integer: combine them with | (OR), test them with & (AND), and add one with |=. This is the classic permission-bits pattern (Read, Write, Execute).
package main
import "fmt"
// Bit flags: each constant is a single bit, so they combine with | (OR).
type Permission uint8
const (
Read Permission = 1 << iota // 1 (binary 001)
Write // 2 (binary 010)
Execute // 4 (binary 100)
)
func main() {
// Combine flags with OR.
p := Read | Write
fmt.Printf("combined = %03b (%d)\n", p, p)
// Test a flag with AND.
fmt.Println("can read? ", p&Read != 0)
fmt.Println("can execute?", p&Execute != 0)
// Add a flag.
p |= Execute
fmt.Printf("now = %03b (%d)\n", p, p)
}🎯 Your Turn
Fill in the blank so Small, Medium, and Large become 0, 1, and 2. Remember that only the first line needs iota — the rest inherit it.
package main
import "fmt"
type Size int
const (
Small Size = ___ // 👉 iota -> 0
Medium // 👉 (blank) -> 1
Large // 👉 (blank) -> 2
)
func main() {
fmt.Println(Small, Medium, Large)
// ✅ Expected output: 0 1 2
}Common Errors (and the fix)
- ❌ "cannot assign to Pi (declared const)" — you tried to reassign a constant. ✅ Constants never change; use a var if you need a value that can be updated.
- ❌ "const initializer ... is not a constant" — you used a function call like time.Now(). ✅ Constants must be compile-time values; move that into a var.
- ❌ "mismatched types int and float64" — you mixed a typed constant with a different type. ✅ Convert explicitly (float64(x)) or leave the constant untyped.
- ❌ iota counts the wrong values — you forgot that iota increments on every line, even blank-skipped ones. ✅ Count lines from the top of the block, not just the ones mentioning iota.
🎯 Mini-Challenge: Reach a Terabyte
Extend the byte-size ladder. In one const block, skip 0, then use 1 << (10 * iota) for KB, MB, GB, and add a TB line. Print TB and match the expected output.
package main
import "fmt"
// 🎯 MINI-CHALLENGE: extend the byte-size ladder to TB.
// Using one const block with iota and 1 << (10 * iota),
// add a TB line after GB and print TB.
//
// ✅ Expected output: 1099511627776
func main() {
// your code here
}🎉 Lesson Complete!
- ✅ const values are fixed at compile time and can't be reassigned
- ✅ Untyped constants adapt to context; typed ones are locked to one type
- ✅ iota resets to 0 per block and increments by 1 each line
- ✅ Skip with _ and shift sequences with arithmetic on iota
- ✅ 1 << iota builds bit flags you combine with | and test with &
Practice quiz
When is a const value fixed?
- At run time
- On first use
- At compile time
- When garbage collected
Answer: At compile time. A constant is fixed at compile time, so the compiler can bake its value directly into the program.
Which of these is a valid constant declaration?
- const Pi = 3.14159
- const start = time.Now()
- const r = rand.Int()
- const now = os.Getenv("X")
Answer: const Pi = 3.14159. Constants must be compile-time values. A function call result like time.Now() is not allowed.
What value does iota have on the first line of a const block?
- 1
- -1
- Whatever you assign
- 0
Answer: 0. iota starts at 0 on the first line of a const block and increments by 1 for each subsequent line.
Given const ( Sunday = iota; Monday; Tuesday; Wednesday ), what is Wednesday?
- 0
- 3
- 1
- 4
Answer: 3. iota is 0 for Sunday, then 1, 2, 3 — so Wednesday is 3.
What does an untyped constant like const big = 1 << 40 give you?
- No fixed type until used; it adapts to context
- A fixed int type
- A float64 always
- A compile error
Answer: No fixed type until used; it adapts to context. An untyped constant has no fixed type until used, so the same constant can become a float64 or an int64.
With 1 << iota for Read, Write, Execute, what are their values?
- 0, 1, 2
- 1, 2, 3
- 1, 2, 4
- 2, 4, 8
Answer: 1, 2, 4. 1 << iota gives 1, 2, 4 — each a distinct single bit, ideal for combinable flags.
Given Read=1, Write=2, what does Read | Write equal?
- 1
- 3
- 2
- 0
Answer: 3. Bitwise OR of 001 and 010 is 011, which is 3.
What does an iota line that omits its expression do?
- Sets the value to 0
- Causes a compile error
- Copies the previous value exactly
- Repeats the previous line's expression with the new iota
Answer: Repeats the previous line's expression with the new iota. A line without an expression repeats the previous line's expression, evaluated with the new iota value.
How do you skip the value 0 at the start of an iota block?
- start = 1
- Assign the first line to _ (e.g. _ = iota)
- Use iota--
- You cannot skip it
Answer: Assign the first line to _ (e.g. _ = iota). Assigning the first line to the blank identifier _ discards 0 so the real values start at the next iota.
Using 1 << (10 * iota) with _ skipping 0, what is the value after KB, MB, GB (the TB line)?
- 1073741824
- 1048576
- 1099511627776
- 549755813888
Answer: 1099511627776. iota reaches 4 on the TB line, so 1 << 40 = 1099511627776.
Continue this course
- Previous: Checkpoint: A Small Program
- Next: Strings & the strings Package — Immutable byte sequences, Split/Join/Replace, and efficient strings.Builder
Frequently asked questions
What is the difference between const and a regular variable?
A const is fixed at compile time and can never be reassigned — the compiler bakes its value into the program. A var holds a value at run time that you can change. Constants must be values the compiler can compute up front (numbers, strings, booleans, and expressions over them); you cannot make a constant from a function call result or a slice.
What does 'typed vs untyped constant' actually mean?
An untyped constant like const x = 5 has no fixed type until you use it, so it can flow into an int, a float64, or any compatible type without a conversion. A typed constant like const x int = 5 is locked to that one type, so combining it with a different type requires an explicit conversion. Leaving constants untyped is usually the more flexible choice.
What is iota and when does it reset?
iota is a built-in counter used inside const blocks. It starts at 0 on the first line of a const block and increases by 1 for each subsequent line (whether or not iota appears on that line). It resets to 0 at the start of every new const block, which makes it perfect for numbering related constants like enums.
Why is 1 << iota used for bit flags?
Shifting 1 left by iota gives you 1, 2, 4, 8, ... — each a single distinct bit. Because no two flags share a bit, you can combine them with | (OR) and test them with & (AND). This lets one integer carry several on/off options at once, like Read|Write.
How do I skip a value with iota?
Assign the line to the blank identifier _, for example '_ = iota' as the first line to throw away 0. You can also skip several values or shift the sequence with arithmetic such as iota + 1. Any line that omits an expression repeats the previous line's expression with the new iota value.