What are Constants?
Constants are immutable values that cannot be changed after declaration. They’re known at compile time and help make code more readable and maintainable.
The Problem with Magic Numbers
Magic numbers are hard-coded values scattered throughout your code:
func main() {
cm := 100
m := cm / 100 // What is 100? Not clear without context
fmt.Printf("%dcm is %dm\n", cm, m)
cm = 200
m = cm / 100 // Same magic number repeated
fmt.Printf("%dcm is %dm\n", cm, m)
}
Problems with magic numbers:
- Unclear meaning
- Easy to make typos
- Hard to update if value changes
- No documentation of intent
The Solution: Named Constants
func main() {
const meters int = 100
cm := 100
m := cm / meters // Clear: we're dividing by meters
fmt.Printf("%dcm is %dm\n", cm, m)
cm = 200
m = cm / meters // Same constant, consistent value
fmt.Printf("%dcm is %dm\n", cm, m)
}
Benefits:
- Self-documenting code
- Single source of truth
- Easy to update
- Prevents accidental changes
Use constants for values that have meaning and won’t change during program execution: configuration values, mathematical constants, status codes, etc.
Declaring Constants
Basic Syntax
Example:
const pi float64 = 3.14159
const maxRetries int = 3
const appName string = "MyApp"
const debugMode bool = true
Type Inference
Like variables, you can omit the type and let Go infer it:
const pi = 3.14159 // float64
const maxRetries = 3 // int
const appName = "MyApp" // string
const debugMode = true // bool
Rules for Constants
1. Immutability
Once declared, constants cannot be changed:
const max = 100
// max = 200 // ✗ Error: cannot assign to max
2. Compile-Time Evaluation
Constants must be known at compile time. You cannot use runtime values or function calls:
// ✓ OK: literal values and expressions
const x = 10
const y = x * 2
const z = "hello " + "world"
// ✗ Error: function calls not allowed
// const now = time.Now()
// ✗ Error: runtime variables not allowed
// var runtime = 10
// const compile = runtime
3. Built-in Functions Allowed
Some built-in functions can be used with constants:
const (
str = "hello"
length = len(str) // ✓ OK: len works with constant strings
)
Allowed functions: len, cap, real, imag, complex
Constants are evaluated at compile time, which means they have zero runtime cost. The compiler replaces constant names with their values.
Constant Types and Expressions
Numeric Constants
const (
a = 10
b = 20
c = a + b // 30
d = a * b / 2 // 100
)
String Constants
const (
first = "Hello"
last = "World"
full = first + " " + last // "Hello World"
)
Boolean Constants
const (
enabled = true
disabled = false
derived = enabled && !disabled // true
)
Multiple Constant Declarations
Grouped Declaration
const (
pi = 3.14159
e = 2.71828
phi = 1.61803
)
This is cleaner and more organized than separate declarations.
Multiple Constants Per Line
const (
width, height = 1920, 1080
x, y, z = 1, 2, 3
)
Repeated Values
When declaring multiple constants, omitting the value repeats the previous expression:
const (
a = 1
b // 1 (same as a)
c // 1 (same as a)
)
const (
x = 10 * 2
y // 20 (same expression as x)
z // 20 (same expression as x)
)
Typed vs Untyped Constants
Untyped Constants
By default, constants are “untyped” - they have a kind but not a specific type:
const min = 1 + 1 // untyped integer
const pi = 3.14 * min // untyped float
fmt.Println(min, pi)
Untyped constants are more flexible and can be used in more contexts.
Typed Constants
You can explicitly specify a type:
const typedInt int = 10
const typedFloat float64 = 3.14
Flexibility of Untyped Constants
Untyped constants can be used with compatible types:
const untypedInt = 10
var i int = untypedInt // ✓ OK
var i8 int8 = untypedInt // ✓ OK
var i64 int64 = untypedInt // ✓ OK
var f float64 = untypedInt // ✓ OK
But typed constants cannot:
const typedInt int = 10
var i int = typedInt // ✓ OK
var i8 int8 = typedInt // ✗ Error: cannot use int as int8
var f float64 = typedInt // ✗ Error: cannot use int as float64
Use untyped constants by default. Only use typed constants when you specifically need to enforce a type.
Default Types
When an untyped constant is used where a type is required, it takes its “default type”:
const x = 10 // untyped integer, default type: int
const y = 3.14 // untyped float, default type: float64
const s = "hi" // untyped string, default type: string
const b = true // untyped bool, default type: bool
fmt.Printf("%T\n", x) // int
fmt.Printf("%T\n", y) // float64
fmt.Printf("%T\n", s) // string
fmt.Printf("%T\n", b) // bool
The iota Identifier
iota is a special identifier used to create sequences of related constants.
Manual Enumeration (Without iota)
const (
monday = 0
tuesday = 1
wednesday = 2
thursday = 3
friday = 4
saturday = 5
sunday = 6
)
This works but is verbose and error-prone.
Using iota
iota starts at 0 and increments by 1 for each constant in the group:
const (
monday = iota // 0
tuesday // 1
wednesday // 2
thursday // 3
friday // 4
saturday // 5
sunday // 6
)
fmt.Println(monday, tuesday, wednesday, thursday, friday, saturday, sunday)
// Output: 0 1 2 3 4 5 6
Start a const block
Use const () for grouped constants
Set first constant to iota
The first constant gets value 0
Omit values for remaining constants
Each subsequent constant increments iota by 1
iota Expressions
You can use expressions with iota:
const (
_ = iota // 0 (ignored with blank identifier)
KB = 1 << (10 * iota) // 1 << (10 * 1) = 1024
MB // 1 << (10 * 2) = 1048576
GB // 1 << (10 * 3) = 1073741824
)
fmt.Println(KB, MB, GB)
// Output: 1024 1048576 1073741824
Skipping Values
Use the blank identifier to skip values:
const (
_ = iota // 0 (skipped)
first // 1
_ // 2 (skipped)
third // 3
)
fmt.Println(first, third)
// Output: 1 3
iota Resets
iota resets to 0 in each new const block:
const (
a = iota // 0
b // 1
)
const (
c = iota // 0 (reset)
d // 1
)
fmt.Println(a, b, c, d)
// Output: 0 1 0 1
iota only works inside const declarations. It cannot be used with variables or outside constant blocks.
Practical Examples
HTTP Status Codes
const (
StatusOK = 200
StatusCreated = 201
StatusAccepted = 202
StatusBadRequest = 400
StatusNotFound = 404
StatusServerError = 500
)
Application States
const (
StateIdle = iota
StateRunning
StatePaused
StateStopped
)
File Permissions
const (
Read = 1 << iota // 1 (binary: 001)
Write // 2 (binary: 010)
Execute // 4 (binary: 100)
)
// Can combine with bitwise OR
const ReadWrite = Read | Write // 3 (binary: 011)
const FullAccess = Read | Write | Execute // 7 (binary: 111)
Configuration Values
const (
appName = "MyApp"
version = "1.0.0"
maxConnections = 100
timeout = 30 // seconds
retryAttempts = 3
debugMode = false
)
Mathematical Constants
const (
pi = 3.14159265359
e = 2.71828182846
phi = 1.61803398875
)
func circleArea(radius float64) float64 {
return pi * radius * radius
}
Best Practices
- Use constants for values that don’t change - Configuration, limits, known values
- Prefer untyped constants - They’re more flexible
- Use iota for enumerations - Cleaner than manual numbering
- Group related constants - Use const blocks for organization
- Name constants clearly - Use descriptive names like
maxRetries not max
- Use UPPERCASE for exported constants - Follow Go naming conventions
- Document non-obvious values - Add comments explaining what constants represent
// Good: clear, organized, documented
const (
// DefaultTimeout is the default request timeout in seconds
DefaultTimeout = 30
// MaxRetries is the maximum number of retry attempts
MaxRetries = 3
// BufferSize is the default buffer size in bytes
BufferSize = 4096
)
Common Patterns
Enumerated Types
type Status int
const (
StatusPending Status = iota
StatusProcessing
StatusComplete
StatusFailed
)
func (s Status) String() string {
switch s {
case StatusPending:
return "Pending"
case StatusProcessing:
return "Processing"
case StatusComplete:
return "Complete"
case StatusFailed:
return "Failed"
default:
return "Unknown"
}
}
Bit Flags
type Permission uint
const (
PermissionNone Permission = 0
PermissionRead Permission = 1 << iota
PermissionWrite
PermissionExecute
PermissionAll = PermissionRead | PermissionWrite | PermissionExecute
)
func (p Permission) Has(perm Permission) bool {
return p&perm == perm
}
Size Constants
const (
_ = iota // ignore first value
KB uint64 = 1 << (10 * iota)
MB
GB
TB
)
func formatBytes(bytes uint64) string {
switch {
case bytes >= TB:
return fmt.Sprintf("%.2f TB", float64(bytes)/float64(TB))
case bytes >= GB:
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
case bytes >= MB:
return fmt.Sprintf("%.2f MB", float64(bytes)/float64(MB))
case bytes >= KB:
return fmt.Sprintf("%.2f KB", float64(bytes)/float64(KB))
default:
return fmt.Sprintf("%d B", bytes)
}
}
Constants are a powerful feature for writing clear, maintainable code. They make your intentions explicit, prevent accidental changes, and have zero runtime cost since they’re evaluated at compile time.
Constants vs Variables
| Feature | Constants | Variables |
|---|
| Can change | No | Yes |
| Evaluation | Compile-time | Runtime |
| Memory | No runtime memory | Uses memory |
| Scope | File, package, or block | File, package, or block |
| Function calls | Only built-ins | Any |
| Performance | Zero overhead | Normal overhead |
When deciding between a constant and a variable, ask: “Will this value ever need to change during program execution?” If no, use a constant.