Constants #
Constants in Go are more than just “variables that can’t change.” Behind the seemingly simple const keyword is a unique type system — untyped constants — that makes Go constants behave very differently from other languages. And there’s iota, a small feature that lets you define expressive enums, bit flags, and sequences of values with no boilerplate at all. Understanding Go constants deeply means understanding why const pi = 3.14 can be used as a float32 and a float64 at the same time without explicit conversion — something ordinary variables can never do.
const vs var — More Than Just “Can’t Change” #
The fundamental difference between const and var isn’t only about mutability:
// var — the value is determined and allocated at runtime
var maxRetry = 3
maxRetry = 5 // ✓ valid — can be changed
// const — the value MUST be evaluable at compile time
const maxRetry = 3
// maxRetry = 5 // ✗ compile error: cannot assign to maxRetry
The key word here is “compile time.” A constant’s value must be fully known to the compiler before the program runs. This means you can’t set a function call result as a constant:
import "time"
// ANTI-PATTERN: compile error — time.Now() is a runtime function
const startTime = time.Now() // ✗ invalid
// CORRECT: this is valid because time.Duration is a constant arithmetic operation
const timeout = 30 * time.Second // ✓ valid — 30 * numeric constant
The benefit of compile-time evaluation isn’t just safety — the compiler can use constant values for optimization. Code using constants is often more efficient because the compiler can “fold” constant expressions directly into machine instructions without memory allocation.
Declaring Constants #
Single Constants #
const pi = 3.14159265358979323846
const greeting = "Welcome to Go"
const maxConnections = 100
const debugMode = false
Constant Blocks #
To define many constants at once, use a const (...) block. This is the recommended way for constants that relate to each other:
const (
// Server configuration
DefaultHost = "localhost"
DefaultPort = 8080
DefaultTimeout = 30 // in seconds
// Application limits
MaxPageSize = 100
MaxFileSize = 10 * 1024 * 1024 // 10 MB in bytes
MaxRetry = 3
// Application version
AppName = "MyGoApp"
AppVersion = "2.1.0"
APIVersion = "v2"
)
Constants with Explicit Types #
const (
Pi float64 = 3.14159265358979323846
E float64 = 2.71828182845904523536
Phi float64 = 1.61803398874989484820 // golden ratio
)
type Weekday int
const Monday Weekday = 1
Typed vs Untyped Constants — Go’s Most Unique Feature #
This is a concept with no equivalent in most other languages, and understanding it unlocks a lot about how Go works.
Untyped Constants #
When you write const x = 42, the constant x is untyped — it doesn’t have a concrete type yet. Instead, it has a “default type” that’s used when no other type context exists. But if there’s a clear type context, it “adapts” to that type:
const x = 42 // untyped integer constant
var i int = x // ✓ x used as int
var i32 int32 = x // ✓ x used as int32
var i64 int64 = x // ✓ x used as int64
var f64 float64 = x // ✓ x used as float64 (42.0)
var c128 complex128 = x // ✓ x used as complex128 (42+0i)
// Compare with an ordinary variable:
var y int = 42
var f float64 = y // ✗ compile error: cannot use y (type int) as type float64
This is why constants in Go are far more flexible than ordinary variables — they aren’t “locked” to a single type.
Typed Constants #
If you include an explicit type, the constant becomes typed and loses the untyped flexibility:
const typedX int = 42
var i int = typedX // ✓
var i64 int64 = typedX // ✗ compile error: cannot use typedX (type int) as type int64
var f float64 = typedX // ✗ compile error
When to Use Typed vs Untyped? #
// Use UNTYPED for mathematical constants and magic numbers
// that might be used across various type contexts
const (
KB = 1024
MB = 1024 * KB
GB = 1024 * MB
)
var fileSize int64 = 10 * GB // ✓ GB used as int64
var bufferSize int = 4 * KB // ✓ KB used as int
var displaySize float64 = 1.5 * GB // ✓ GB used as float64
// Use TYPED for enums and constants with a specific type
type Direction int
const (
North Direction = iota
South
East
West
)
// Typed lets the compiler prevent careless usage:
// func move(d Direction) {}
// move(1) // ✗ compile error: cannot use 1 (untyped int constant) as Direction
Unlimited Precision of Untyped Numeric Constants #
Here’s a small wonder rarely noticed: untyped numeric constants in Go store values with unlimited precision. They aren’t limited by bit width like int64 or float64 — their values are stored as pure mathematical numbers during compile time.
// This constant stores pi with all 20 decimal digits
const pi = 3.14159265358979323846
// When used, precision adapts to the target type
var f32 float32 = pi // 3.1415927 (7 digits, float32 precision)
var f64 float64 = pi // 3.141592653589793 (15-17 digits, float64 precision)
// Arithmetic on constants also uses unlimited precision
const bigNumber = 1 << 100 // 2^100 — no overflow!
// bigNumber is a valid constant even though it far exceeds int64 max
// But when used as a specific type, there are limits:
// var n int64 = bigNumber // ✗ overflow — 2^100 doesn't fit in int64
const smallEnough = 1 << 62 // still fits in int64
var n int64 = smallEnough // ✓
This also means constant expressions are always mathematically accurate — no floating-point rounding errors as long as they stay at compile time:
const exactThird = 1.0 / 3.0 // very high precision at compile time
// Rounding only happens when assigned to a float64:
var approxThird float64 = exactThird // 0.3333333333333333
iota — Go’s Automatic Counter #
iota is a special identifier only valid inside a const block. Its value starts at 0 in each new const block and increments by 1 for each constant line in the block. The automatic increment flow of iota can be visualized as follows:
flowchart TD
ConstStart["Start const (...)" Block] --> Row0["Line 1: Zero = iota (iota = 0)"]
Row0 --> Row1["Line 2: One (iota = 1)"]
Row1 --> Row2["Line 3: Two (iota = 2)"]
Row2 --> Row3["Line 4: Three (iota = 3)"]
Row3 --> ConstEnd["Exit const Block"]const (
Zero = iota // 0
One // 1 — iota increments automatically
Two // 2
Three // 3
Four // 4
)
fmt.Println(Zero, One, Two, Three, Four) // 0 1 2 3 4
iota Resets in Every const Block #
iota always starts from 0 in each new const block, not continuing from the previous block:
const (
A = iota // 0
B // 1
C // 2
)
const (
X = iota // 0 — iota RESETS, starts from 0 again
Y // 1
Z // 2
)
Skipping Values with the Blank Identifier #
If you don’t want the value 0 (often confusing for enums because it’s the zero value), use _ to skip it:
type LogLevel int
const (
_ LogLevel = iota // 0 — skipped, no name for 0
DEBUG // 1
INFO // 2
WARNING // 3
ERROR // 4
FATAL // 5
)
Now var level LogLevel (zero value = 0) will never match one of the valid levels — useful for detecting variables that were forgotten to initialize.
Skipping Multiple Values #
const (
_ = iota // 0 — skipped
_ // 1 — skipped
_ // 2 — skipped
ImportantValue // 3
_ // 4 — skipped
AnotherValue // 5
)
iota with Expressions — The Real Power #
iota shows its true power when combined with expressions. The same expression is applied to each iota value automatically:
Bit Flags with Bit Shifts #
The most classic pattern: each constant is a different power of two, perfect for permission flags:
type Permission uint8
const (
PermRead Permission = 1 << iota // 1 << 0 = 1 (binary: 00000001)
PermWrite // 1 << 1 = 2 (binary: 00000010)
PermExecute // 1 << 2 = 4 (binary: 00000100)
PermAdmin // 1 << 3 = 8 (binary: 00001000)
PermDelete // 1 << 4 = 16 (binary: 00010000)
)
func hasPermission(userPerm, checkPerm Permission) bool {
return userPerm&checkPerm != 0
}
func main() {
// Combine permissions with bitwise OR
editorPerm := PermRead | PermWrite
adminPerm := PermRead | PermWrite | PermExecute | PermAdmin | PermDelete
fmt.Println(hasPermission(editorPerm, PermRead)) // true
fmt.Println(hasPermission(editorPerm, PermAdmin)) // false
fmt.Println(hasPermission(adminPerm, PermDelete)) // true
// Remove a permission with bitwise AND NOT
editorPerm &^= PermWrite
fmt.Println(hasPermission(editorPerm, PermWrite)) // false
}
Storage Units #
type ByteSize float64
const (
_ = iota // ignore 0
KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
MB // 1 << 20 = 1,048,576
GB // 1 << 30 = 1,073,741,824
TB // 1 << 40
PB // 1 << 50
)
func main() {
fmt.Printf("1 KB = %.0f bytes\n", float64(KB))
fmt.Printf("1 MB = %.0f bytes\n", float64(MB))
fmt.Printf("1 GB = %.0f bytes\n", float64(GB))
fileSize := 2.5 * float64(GB)
fmt.Printf("File size: %.2f GB\n", fileSize/float64(GB))
}
Starting from a Value Other Than 0 #
type HTTPStatus int
const (
StatusOK HTTPStatus = 200 + iota // 200
StatusCreated // 201
StatusAccepted // 202
StatusNonAuthoritativeInfo // 203
StatusNoContent // 204
)
// Or a separate group
const (
StatusBadRequest HTTPStatus = 400 + iota // 400
StatusUnauthorized // 401
StatusForbidden HTTPStatus = 403 // 403 — explicit value, resets iota path
StatusNotFound HTTPStatus = 404
)
Complete Enum Pattern with a String() Method #
Go doesn’t have a built-in enum type, but the idiomatic pattern of iota plus a String() method gives a nearly equivalent experience. The String() method lets enum values print with meaningful names, not just numbers:
package main
import "fmt"
// Enum definition
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
// String() method — automatically called by fmt when printing
func (d Weekday) String() string {
names := [...]string{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
}
if d < Sunday || d > Saturday {
return fmt.Sprintf("Weekday(%d)", int(d))
}
return names[d]
}
// Additional methods that make the enum more useful
func (d Weekday) IsWeekend() bool {
return d == Saturday || d == Sunday
}
func (d Weekday) Next() Weekday {
return (d + 1) % 7 // wrap around — after Saturday comes Sunday
}
func main() {
today := Wednesday
fmt.Println(today) // Wednesday — not 3
fmt.Println(today.IsWeekend()) // false
fmt.Println(today.Next()) // Thursday
for d := Sunday; d <= Saturday; d++ {
if d.IsWeekend() {
fmt.Printf("%s is a weekend day\n", d)
}
}
}
Mixed Expressions in a const Block #
Lines in a const block don’t all have to use iota. You can mix them with explicit values:
const (
A = iota // 0
B // 1
C = 100 // 100 — explicit value, iota still increments to 2
D = iota // 3 — iota continues from its position, not from C
E // 4
)
fmt.Println(A, B, C, D, E) // 0 1 100 3 4
This pattern is rarely used because it’s confusing. If you need a different value in the middle, it’s better to split it into two const blocks.
Constants in Production Code #
Sentinel Values — Values with Specific Meaning #
package database
import "errors"
// Sentinel errors — errors checkable with errors.Is()
var (
ErrNotFound = errors.New("record not found")
ErrDuplicate = errors.New("record already exists")
ErrUnauthorized = errors.New("no access")
)
// Sentinel constants for special conditions
const (
NoID = 0
NoLimit = -1 // for unlimited queries
NoOffset = 0
)
func GetUser(id int) (*User, error) {
if id == NoID {
return nil, ErrNotFound
}
// ...
}
Configuration Constants #
package config
const (
// HTTP
DefaultHTTPPort = 8080
DefaultHTTPSPort = 8443
DefaultReadTimeout = 30 // seconds
DefaultWriteTimeout = 30 // seconds
DefaultIdleTimeout = 120 // seconds
// Database
DefaultDBMaxOpenConns = 25
DefaultDBMaxIdleConns = 25
DefaultDBConnMaxLife = 5 // minutes
// Cache
DefaultCacheTTL = 300 // seconds (5 minutes)
DefaultCacheMaxSize = 1000 // entries
// Pagination
DefaultPageSize = 20
MaxPageSize = 100
MinPageSize = 1
)
Avoid Magic Numbers — Use Named Constants #
// ANTI-PATTERN: magic numbers — what does 86400 mean? why 20? why 100?
func processRequest(size int) bool {
if size > 104857600 { // what is this?
return false
}
time.Sleep(300 * time.Millisecond) // why 300?
return true
}
// CORRECT: named constants make code self-documenting
const (
MaxRequestBodySize = 100 * 1024 * 1024 // 100 MB in bytes
RequestProcessDelay = 300 * time.Millisecond
)
func processRequest(size int) bool {
if size > MaxRequestBodySize { // meaning is clear
return false
}
time.Sleep(RequestProcessDelay) // purpose is clear
return true
}
Complete Example Program #
The following program combines various constant concepts in an access management system scenario:
package main
import "fmt"
// Types for roles and permissions
type Role int
type Permission uint16
// Role enum with iota and a String() method
const (
RoleGuest Role = iota
RoleUser
RoleModerator
RoleAdmin
RoleSuperAdmin
)
func (r Role) String() string {
switch r {
case RoleGuest:
return "Guest"
case RoleUser:
return "User"
case RoleModerator:
return "Moderator"
case RoleAdmin:
return "Admin"
case RoleSuperAdmin:
return "SuperAdmin"
default:
return fmt.Sprintf("Role(%d)", int(r))
}
}
// Bit flags for permissions
const (
PermNone Permission = 0
PermRead Permission = 1 << iota // 1
PermCreate // 2
PermUpdate // 4
PermDelete // 8
PermPublish // 16
PermManageUsers // 32
PermViewAuditLog // 64
// Common permission combinations
PermReadOnly = PermRead
PermEditor = PermRead | PermCreate | PermUpdate
PermPublisher = PermEditor | PermPublish
PermAdminAll = PermPublisher | PermDelete | PermManageUsers | PermViewAuditLog
)
// System limits
const (
MaxUsersPerOrg = 1000
MaxRolesPerUser = 5
TokenExpirySecs = 3600 // 1 hour
PasswordMinLen = 8
PasswordMaxLen = 128
)
type User struct {
Name string
Role Role
Permissions Permission
}
func (u User) Can(perm Permission) bool {
return u.Permissions&perm != 0
}
func (u User) String() string {
return fmt.Sprintf("%s (%s)", u.Name, u.Role)
}
func defaultPermissionsForRole(role Role) Permission {
switch role {
case RoleGuest:
return PermNone
case RoleUser:
return PermReadOnly
case RoleModerator:
return PermEditor
case RoleAdmin:
return PermPublisher | PermManageUsers
case RoleSuperAdmin:
return PermAdminAll
default:
return PermNone
}
}
func main() {
// Create users with default permissions based on role
users := []User{
{Name: "Guest", Role: RoleGuest, Permissions: defaultPermissionsForRole(RoleGuest)},
{Name: "Budi", Role: RoleUser, Permissions: defaultPermissionsForRole(RoleUser)},
{Name: "Sari", Role: RoleModerator, Permissions: defaultPermissionsForRole(RoleModerator)},
{Name: "Ahmad", Role: RoleAdmin, Permissions: defaultPermissionsForRole(RoleAdmin)},
}
fmt.Println("=== User Access Report ===")
fmt.Printf("User limit per organization: %d\n\n", MaxUsersPerOrg)
actions := []struct {
name string
perm Permission
}{
{"Read content", PermRead},
{"Create content", PermCreate},
{"Delete content", PermDelete},
{"Manage users", PermManageUsers},
}
for _, user := range users {
fmt.Printf("User: %s\n", user)
for _, action := range actions {
status := "✗ not allowed"
if user.Can(action.perm) {
status = "✓ allowed"
}
fmt.Printf(" %-25s %s\n", action.name, status)
}
fmt.Println()
}
}
Program output:
=== User Access Report ===
User limit per organization: 1000
User: Guest (Guest)
Read content ✗ not allowed
Create content ✗ not allowed
Delete content ✗ not allowed
Manage users ✗ not allowed
User: Budi (User)
Read content ✓ allowed
Create content ✗ not allowed
Delete content ✗ not allowed
Manage users ✗ not allowed
User: Sari (Moderator)
Read content ✓ allowed
Create content ✓ allowed
Delete content ✗ not allowed
Manage users ✗ not allowed
User: Ahmad (Admin)
Read content ✓ allowed
Create content ✓ allowed
Delete content ✗ not allowed
Manage users ✓ allowed
Summary #
- Constants are evaluated at compile time — their values must be fully known to the compiler; you can’t use runtime function results.
- Untyped constants are flexible — usable across various type contexts without explicit conversion; great for numbers and string literals.
- Typed constants are bound to a specific type — useful for enums so the compiler prevents careless usage.
- Untyped numeric constants have unlimited precision at compile time — no overflow or rounding errors as long as they remain constants.
iotais an automatic counter in const blocks — starts at0, increments by1per line, and resets in every newconstblock._skips unwanted iota values — usually to avoid an ambiguous zero value being treated as “not initialized.”1 << iotais the classic bit-flags pattern — every constant is a unique power of two.- A
String()method lets enum types print meaningful names instead of numbers.- Use named constants, not magic numbers — code becomes self-documenting and easier to maintain.
- Choose
constovervarfor values that truly never change — it’s a clear communication of intent to readers and prevents accidental changes.