Constants (const)
What is a Constant?β
A constant is a value that cannot change during program execution. In Go, constants are resolved at compile time, so values that are only known at runtime cannot be declared as constants.
Differences from variables (var):
| Aspect | Variable (var) | Constant (const) |
|---|---|---|
| Value change | Allowed | Not allowed |
| When resolved | Runtime | Compile time |
| Initial value | Optional (zero value) | Required |
| Address reference | Allowed (&x) | Not allowed |
| Types | All types | Basic types only |
Constants are typically used for mathematical constants, configuration values, and enumeration values.
Declaring Constantsβ
Single Constantβ
package main
import "fmt"
const Pi = 3.14159265358979
const AppName = "MyApp"
const MaxSize = 1024
const IsDebug = false
func main() {
fmt.Println(Pi, AppName, MaxSize, IsDebug)
// Calculate circle area
radius := 5.0
area := Pi * radius * radius
fmt.Printf("Area of circle with radius %.0f: %.4f\n", radius, area)
}
Block Constant Declarationβ
Use a block form when declaring multiple constants at once.
package main
import "fmt"
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
DefaultTimeout = 30
MaxRetries = 3
)
func main() {
fmt.Println("HTTP Status:", StatusOK, StatusNotFound, StatusError)
fmt.Printf("Timeout: %ds, Max retries: %d\n", DefaultTimeout, MaxRetries)
}
Typed vs Untyped Constantsβ
Go constants come in two varieties: typed constants and untyped constants.
Untyped Constants β More Flexibleβ
A constant declared without an explicit type has high precision and its type is determined by the context in which it is used.
package main
import "fmt"
const (
// Untyped constants β type determined by context
UntypedInt = 42
UntypedFloat = 3.14
UntypedString = "hello"
UntypedBool = true
)
func main() {
// UntypedInt can be used as int, int32, int64, float64, etc.
var i int = UntypedInt
var i32 int32 = UntypedInt
var i64 int64 = UntypedInt
var f float64 = UntypedInt // looks like int, but also valid as float64!
fmt.Println(i, i32, i64, f)
// Untyped integer constants support very high precision
const huge = 1_000_000_000_000_000_000 // 10^18, within int64 range
fmt.Println(huge)
}
Typed Constants β More Strictβ
package main
import "fmt"
const (
// Typed constants β only usable as their specified type
TypedInt int = 42
TypedFloat float64 = 3.14
TypedString string = "hello"
)
func main() {
var i int = TypedInt
// var i32 int32 = TypedInt // compile error! can't assign int constant to int32
var i32 int32 = int32(TypedInt) // explicit conversion required
fmt.Println(i, i32)
// Typed constants only allow operations for their type
result := TypedFloat * 2 // OK: float64 * 2
fmt.Println(result)
}
iota β Auto-Incrementing Enumeratorβ
iota is a special identifier used inside const blocks. It starts at 0 for the first constant in the block and increments by 1 for each subsequent constant.
Basic iotaβ
package main
import "fmt"
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func main() {
fmt.Println("Sunday:", Sunday) // 0
fmt.Println("Monday:", Monday) // 1
fmt.Println("Saturday:", Saturday) // 6
today := Wednesday
if today == Wednesday {
fmt.Println("Today is Wednesday.")
}
}
iota Expressions: Bit Flagsβ
Using iota in expressions enables more complex patterns.
package main
import "fmt"
// File permission bit flags (Unix style)
const (
Read = 1 << iota // 1 << 0 = 1 (0b001)
Write // 1 << 1 = 2 (0b010)
Execute // 1 << 2 = 4 (0b100)
)
// Network feature flags
const (
FeatureHTTP = 1 << iota // 1
FeatureHTTPS // 2
FeatureWebSocket // 4
FeatureGRPC // 8
)
func main() {
// Individual permissions
fmt.Printf("Read: %d (0b%03b)\n", Read, Read)
fmt.Printf("Write: %d (0b%03b)\n", Write, Write)
fmt.Printf("Execute: %d (0b%03b)\n", Execute, Execute)
// Combined permissions (bitwise OR)
readWrite := Read | Write // 3 (0b011)
allPerms := Read | Write | Execute // 7 (0b111)
fmt.Printf("Read+Write: %d (0b%03b)\n", readWrite, readWrite)
fmt.Printf("All perms: %d (0b%03b)\n", allPerms, allPerms)
// Check a permission (bitwise AND)
perm := Read | Execute // 5 (0b101)
if perm&Write != 0 {
fmt.Println("Write permission granted")
} else {
fmt.Println("Write permission denied")
}
// Check network features
enabledFeatures := FeatureHTTP | FeatureWebSocket
fmt.Printf("HTTP: %v\n", enabledFeatures&FeatureHTTP != 0) // true
fmt.Printf("HTTPS: %v\n", enabledFeatures&FeatureHTTPS != 0) // false
fmt.Printf("WebSocket: %v\n", enabledFeatures&FeatureWebSocket != 0) // true
}
Skipping the First Value with _β
package main
import "fmt"
type LogLevel int
const (
_ = iota // skip 0
LevelDebug // 1
LevelInfo // 2
LevelWarn // 3
LevelError // 4
LevelFatal // 5
)
func (l LogLevel) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
case LevelFatal:
return "FATAL"
default:
return "UNKNOWN"
}
}
func main() {
level := LevelInfo
fmt.Printf("Level value: %d, name: %s\n", level, level) // 2, INFO
// Only log at currentLevel or above
currentLevel := LevelWarn
logs := []struct {
level LogLevel
message string
}{
{LevelDebug, "debug message"},
{LevelInfo, "info message"},
{LevelWarn, "warning message"},
{LevelError, "error message"},
}
for _, log := range logs {
if log.level >= currentLevel {
fmt.Printf("[%s] %s\n", log.level, log.message)
}
}
}
iota Expressions: More Patternsβ
package main
import "fmt"
// Powers of 1024
const (
_ = iota // 0
KB = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1048576
GB // 1 << 30 = 1073741824
TB // 1 << 40 = 1099511627776
)
// Multiplied by 10
const (
Level1 = (iota + 1) * 10 // 10
Level2 // 20
Level3 // 30
Level4 // 40
Level5 // 50
)
func main() {
fmt.Printf("1 KB = %d bytes\n", KB)
fmt.Printf("1 MB = %d bytes\n", MB)
fmt.Printf("1 GB = %d bytes\n", GB)
fileSize := int64(2.5 * float64(GB))
fmt.Printf("File size: %.1f GB\n", float64(fileSize)/float64(GB))
fmt.Println("Level XP thresholds:", Level1, Level2, Level3, Level4, Level5)
}
Independence of Multiple iota Blocksβ
Each const block has its own independent iota. Starting a new block resets iota to 0.
package main
import "fmt"
const (
A = iota // 0
B // 1
C // 2
)
const (
X = iota // 0 β resets to 0 in the new block!
Y // 1
Z // 2
)
func main() {
fmt.Println(A, B, C) // 0 1 2
fmt.Println(X, Y, Z) // 0 1 2
}
Real-World Example: Weekday Enumerationβ
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
var weekdayNames = [...]string{
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday",
}
func (d Weekday) String() string {
if d < Sunday || d > Saturday {
return "Unknown"
}
return weekdayNames[d]
}
func (d Weekday) IsWeekend() bool {
return d == Sunday || d == Saturday
}
func main() {
today := Wednesday
fmt.Printf("Today: %s\n", today)
fmt.Printf("Is weekend: %v\n", today.IsWeekend())
// Print the whole week
for day := Sunday; day <= Saturday; day++ {
weekend := ""
if day.IsWeekend() {
weekend = " (weekend)"
}
fmt.Printf("%d: %s%s\n", day, day, weekend)
}
}
Constant Expressionsβ
Constants can include expressions that are evaluated at compile time.
package main
import "fmt"
const (
SecondsPerMinute = 60
MinutesPerHour = 60
HoursPerDay = 24
DaysPerWeek = 7
// Derived constants β computed at compile time
SecondsPerHour = SecondsPerMinute * MinutesPerHour // 3600
SecondsPerDay = SecondsPerHour * HoursPerDay // 86400
SecondsPerWeek = SecondsPerDay * DaysPerWeek // 604800
)
func main() {
fmt.Println("1 minute =", SecondsPerMinute, "seconds")
fmt.Println("1 hour =", SecondsPerHour, "seconds")
fmt.Println("1 day =", SecondsPerDay, "seconds")
fmt.Println("1 week =", SecondsPerWeek, "seconds")
// Convert total seconds to human-readable form
totalSeconds := 90_000
days := totalSeconds / SecondsPerDay
hours := (totalSeconds % SecondsPerDay) / SecondsPerHour
minutes := (totalSeconds % SecondsPerHour) / SecondsPerMinute
seconds := totalSeconds % SecondsPerMinute
fmt.Printf("%d seconds = %dd %dh %dm %ds\n",
totalSeconds, days, hours, minutes, seconds)
}
Constants and switch Patternsβ
iota-based enumerations pair naturally with switch statements.
package main
import "fmt"
type Direction int
const (
North Direction = iota
East
South
West
)
func (d Direction) String() string {
return [...]string{"North", "East", "South", "West"}[d]
}
func (d Direction) Opposite() Direction {
return (d + 2) % 4
}
func move(d Direction, steps int) string {
switch d {
case North:
return fmt.Sprintf("Move %d steps north", steps)
case East:
return fmt.Sprintf("Move %d steps east", steps)
case South:
return fmt.Sprintf("Move %d steps south", steps)
case West:
return fmt.Sprintf("Move %d steps west", steps)
default:
return "Unknown direction"
}
}
func main() {
d := North
fmt.Printf("Current direction: %s\n", d)
fmt.Printf("Opposite: %s\n", d.Opposite())
fmt.Println(move(East, 5))
fmt.Println(move(South, 3))
}
Why Go Has No enum Keywordβ
Go does not provide a dedicated enum keyword like C, Java, or Rust. Instead, you implement enumerations with the const + iota + type definition combination.
Advantages of this approach:
- Methods can be added: defining a type lets you attach methods like
String()andIsValid()naturally. - Type safety: the defined type reduces the risk of passing invalid values.
- Simplicity: powerful enumerations are achievable using existing language constructs β no extra keyword needed.
package main
import "fmt"
type Status int
const (
StatusPending Status = iota
StatusActive
StatusInactive
StatusDeleted
)
func (s Status) IsValid() bool {
return s >= StatusPending && s <= StatusDeleted
}
func (s Status) String() string {
names := []string{"pending", "active", "inactive", "deleted"}
if !s.IsValid() {
return fmt.Sprintf("unknown(%d)", int(s))
}
return names[s]
}
func processUser(status Status) {
if !status.IsValid() {
fmt.Println("Invalid status:", status)
return
}
fmt.Printf("Processing user with status: %s\n", status)
}
func main() {
processUser(StatusActive)
processUser(StatusDeleted)
processUser(Status(99)) // invalid value
}
Pro Tipsβ
Tip 1: Design the zero value of iota to mean "unset" or "unknown"
By convention, 0 in an enumeration should represent "uninitialized" or "unknown".
type OrderStatus int
const (
OrderStatusUnknown OrderStatus = iota // 0 β uninitialized
OrderStatusPending // 1
OrderStatusShipped // 2
OrderStatusDelivered // 3
)
// var o OrderStatus automatically becomes OrderStatusUnknown (0)
Tip 2: Prefer untyped constants for flexible APIs
Untyped constants are compatible with multiple numeric types, making your API more ergonomic.
const Timeout = 30 // untyped int constant
var t1 int = Timeout // OK
var t2 int64 = Timeout // OK
var t3 float64 = Timeout // OK
Tip 3: Don't repeat the package name in constant names
The package name already serves as a namespace prefix β avoid redundancy.
// Bad: http.HTTPStatusOK (HTTP duplicated)
// Good: http.StatusOK
Tip 4: Be careful when serializing iota-based values
iota values are determined by their position in the source code. If you store the numeric values in a database or on a network, inserting a new constant in the middle will shift the values of all subsequent constants, silently corrupting existing data. Use explicit values in that case.
// Risky: inserting in the middle changes existing values
const (
A = iota // 0
// Adding a constant here would change the values of B and C!
B // 1
C // 2
)
// Safe: use explicit values
const (
A = 1
B = 2
C = 3
)