Skip to main content

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):

AspectVariable (var)Constant (const)
Value changeAllowedNot allowed
When resolvedRuntimeCompile time
Initial valueOptional (zero value)Required
Address referenceAllowed (&x)Not allowed
TypesAll typesBasic 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:

  1. Methods can be added: defining a type lets you attach methods like String() and IsValid() naturally.
  2. Type safety: the defined type reduces the risk of passing invalid values.
  3. 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
)