Constants and Literals
Constants and Literals
A constant is a value the compiler knows. The boring default is const for names that must not change (prices, bit sizes, shift labels), untyped until a type is useful, and iota only for a dense list of related integers.
Mental model
const n = 4 is untyped. It can become int or int64 at the use site. const n int = 4 is typed and stays int.
iota is a counter that starts at 0 in each const block and goes up by one per line. A line with no expression repeats the previous expression, which is how iota “fills down.”
Numeric literals may use _ as a digit separator. Rune literals are code points in single quotes: 'A', '€', '\n'.
Worked examples
Case 1: Named prices that cannot drift
Save as menu_const.go. Cents are integers. The names are the documentation.
// menu_const.go
package main
import "fmt"
const (
toastCents = 350
teaCents = 250
soupCents = 600
)
func main() {
total := toastCents + teaCents
fmt.Println(total)
fmt.Printf("%T\n", toastCents)
}Run:
go run menu_const.goOutput:
600
int
toastCents is untyped until fmt.Printf("%T") gives it the default type int. You still cannot assign to it: there is no toastCents = 1.
Case 2: Untyped vs typed constants
Save as typed_const.go. The untyped seat count fits both widths. The typed one needs a conversion.
// typed_const.go
package main
import "fmt"
func main() {
const seats = 4
var i int = seats
var w int64 = seats
const typedSeats int = 4
var also int64 = int64(typedSeats)
fmt.Println(i, w, also)
}Run:
go run typed_const.goOutput:
4 4 4
var also int64 = typedSeats does not compile. Typed constants follow the same conversion rules as typed variables.
Case 3: iota for a small set of states
Save as ticket_state.go. Consecutive integers with names beat magic 0 and 1 in a switch.
// ticket_state.go
package main
import "fmt"
const (
StateOpen = iota
StateSeated
StateOrdered
StatePaid
)
func label(s int) string {
switch s {
case StateOpen:
return "open"
case StateSeated:
return "seated"
case StateOrdered:
return "ordered"
case StatePaid:
return "paid"
default:
return "unknown"
}
}
func main() {
fmt.Println(StateOpen, label(StateOpen))
fmt.Println(StatePaid, label(StatePaid))
}Run:
go run ticket_state.goOutput:
0 open
3 paid
Skip a value with _: const ( _ = iota; Morning; Evening ) gives Morning = 1, Evening = 2.
Case 4: Numeric literals and rune literals
Save as literals.go. Underscores are for readers. Bases are 0b, 0o, 0x. A rune is an integer.
// literals.go
package main
import "fmt"
func main() {
cents := 1_250
flags := 0b_0000_0101
table := 0x0C
euro := '€'
fmt.Println(cents)
fmt.Println(flags)
fmt.Println(table)
fmt.Printf("%q %d %U\n", euro, euro, euro)
}Run:
go run literals.goOutput:
1250
5
12
'€' 8364 U+20AC
1_250 is one thousand two hundred fifty, not “a tuple.” The underscores are not digits.
Case 5: Bit flags with 1 << iota
Save as bit_flags.go. When modeling permissions, roles, or operational modes, shifting 1 by iota creates distinct power-of-two bit flags that can be combined with bitwise OR (|) and tested with bitwise AND (&).
// bit_flags.go
package main
import "fmt"
const (
PermRead = 1 << iota // 1 << 0 == 1
PermWrite // 1 << 1 == 2
PermAudit // 1 << 2 == 4
)
func main() {
role := PermRead | PermWrite
hasRead := role&PermRead != 0
hasAudit := role&PermAudit != 0
fmt.Println("role flags:", role)
fmt.Println("can read:", hasRead)
fmt.Println("can audit:", hasAudit)
}Run:
go run bit_flags.goOutput:
role flags: 3
can read: true
can audit: false
Bitmasks let you represent multi-attribute status in a single integer without slices or maps.
Case 6: Arbitrary precision in constant math
Save as big_const.go. Untyped constants in Go are evaluated at compile time with arbitrary precision (at least 256 bits of precision). Intermediate expressions can exceed the limits of int64 without overflowing, as long as the final assigned value fits the target type.
// big_const.go
package main
import "fmt"
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
)
func main() {
fmt.Println("KiB:", KiB)
fmt.Println("MiB:", MiB)
fmt.Println("GiB:", GiB)
// Intermediate calculation exceeds int64, but result fits in int
const huge = (1 << 100) / (1 << 90)
fmt.Println("huge ratio:", huge)
}Run:
go run big_const.goOutput:
KiB: 1024
MiB: 1048576
GiB: 1073741824
huge ratio: 1024
1 << 100 would immediately overflow a 64-bit variable, but in constant arithmetic it evaluates cleanly.
The trap
iota keeps counting even when you write a different expression. The next line repeats that expression, not “the next iota.” Save as iota_repeat.go:
// iota_repeat.go
package main
import "fmt"
func main() {
const (
open = iota // 0
seated // 1
hold = 10
ordered // 10 again — repeats 10, iota is unused
paid = iota // 4 — iota still counted the lines
)
fmt.Println(open, seated, hold, ordered, paid)
}Run:
go run iota_repeat.goOutput:
0 1 10 10 4
ordered is not 3. It is 10. If you need a hole, use _ = iota or write every value. If the numbers must be stable on the wire, write the numbers (StatePaid = 3) and stop using iota for that block.
The boring rule
constfor values that are part of the program, not the request.- Leave constants untyped unless a typed const documents a width.
iotafor dense, internal enumerations and bit flags (1 << iota).- Do not use
iotafor wire protocol or database codes that must remain immutable across versions. - Rely on compile-time arbitrary precision for byte size calculations (
1 << 20for MiB). - Use
_in long numbers:1_000_000, not1000000. - Rune literals for code points. String literals for text.
- One
constblock per family of names.
Try this
- In
menu_const.go, trytoastCents = 1insidemain. Read the compile error. - In
bit_flags.go, addPermAdmin = 1 << iotaand check if a user withPermAdmin | PermReadhas admin privileges. - In
big_const.go, calculateTiBas the next line in theiotablock. - Add a typed
const soupCents int64 = 600and add it to aninttotal. Convert one side. - In
ticket_state.go, insert_ = iotaas the first line soStateOpenbecomes 1.