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.
  • iota is an automatic counter in const blocks — starts at 0, increments by 1 per line, and resets in every new const block.
  • _ skips unwanted iota values — usually to avoid an ambiguous zero value being treated as “not initialized.”
  • 1 << iota is 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 const over var for values that truly never change — it’s a clear communication of intent to readers and prevents accidental changes.

← Previous: Variables   Next: Data Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact