Constants in Go

Learn how to declare and use constants in Go, including iota, constant expressions, and the default types of untyped constants.

A constant in Go is a value that stays the same for the entire life of a program. You give a name to a number, a string, or a boolean, and that name can never be reassigned. Constants are evaluated at compile time — the compiler must be able to work out their value without running the program. This restriction makes them safe, predictable, and easy for the compiler to optimize.

Constants exist to replace “magic numbers” and repeated literals with a single, descriptive name. When you use const MaxRetries = 3 instead of scattering the number 3 through your code, you get two things at once: the meaning is obvious, and if the limit ever changes, you only edit one line.

Go’s constant system is unusual in one important way: constants can be untyped. An untyped constant doesn’t yet have a concrete Go type like int or float64. It lives in a more flexible space that lets you use it with different types without explicit conversions — as long as the value is representable in the target type. This idea sits at the center of how constants work in Go, and we’ll return to it in every section that follows.

Constant Declarations

Constants are declared with the const keyword. A constant can hold a boolean, a numeric value (integer, float, or complex), or a string. You cannot declare a constant for a struct, a slice, or a function — only those basic kinds of values.

The simplest declaration binds a name to a literal:

const Greeting = "Hello"
const MaxItems = 100
const IsEnabled = true

These three constants are untyped because no type appears after the name. They are pure values, not yet locked to a specific Go type.

When you do want to fix a specific type, place it before the = sign:

const Greeting string = "Hello"
const MaxItems int = 100

Now Greeting is a value of type string, and MaxItems is a value of type int. The difference matters because typed constants obey all the strict type rules of Go — you cannot assign a typed int constant to a float64 variable without an explicit conversion, whereas an untyped integer constant would work without complaint.

const Pi = 3.14159
const Greeting = "Hello"
var f float64 = Pi      // OK, Pi is representable as float64
var s string = Greeting  // OK

Untyped constants adapt to the type that needs them — no conversion required.

Constants are often grouped into a block for readability. Each line inside the block may declare one or more constants, and you can mix typed and untyped declarations:

const (
    StatusOK   = 200
    StatusErr  = 500
    AppName    string = "go-server"
    MaxTimeout        = 30
)

You can declare constants at the package level (outside any function) or inside a function. Package-level constants are visible throughout the package; constants inside a function are local to that function. The declaration order between two package-level constants usually doesn’t matter, except when one constant refers to another — the referenced constant must appear earlier or be part of the same block.

Multiple constants can appear on a single line as long as they share the same type:

const x, y int = 10, 20

`:=` does not work for constants:

The short variable declaration operator := is only for variables. Using const x := 5 is a syntax error. Always use const x = 5 or const x int = 5.

A constant’s value must be known at compile time. You cannot call a regular function, dereference a pointer, or read a variable to compute a constant. This is why const now = time.Now() is illegal — time.Now() runs at runtime.

The iota Identifier

iota is a predeclared identifier that makes it easy to create sequences of related constants. Think of it as an automatic counter that starts at 0 inside each const block and increments by 1 after each constant specification line.

The classic use is defining an enumeration:

const (
    Red = iota   // 0
    Green        // 1
    Blue         // 2
)

iota resets to 0 every time a new const block begins, so you can define multiple independent sequences in different blocks.

1

Build an enumeration with iota

const (
    Read   = iota // iota starts at 0
    Write         // implicitly iota again, now 1
    Execute       // 2
)

Each subsequent identifier automatically receives the incrementing value. You do not need to repeat iota on every line — only the first one is required to establish the pattern.

2

Add expressions to create bitmasks or offsets

const (
    FlagRead  = 1 << iota  // 1 << 0 = 1
    FlagWrite              // 1 << 1 = 2
    FlagExec               // 1 << 2 = 4
)

Here iota is used inside an expression. The expression is re-evaluated for each line, so each constant gets a distinct power of two. This is a common pattern for permission flags.

3

Skip values with the blank identifier

const (
    _ = iota // skip 0
    KB = 1 << (10 * iota) // 1 << (10*1) = 1024
    MB                     // 1 << (10*2) = 1_048_576
    GB                     // 1 << (10*3)
)

If you need to discard a value while keeping the counter running, assign iota to _. The counter still advances.

iota only works inside const blocks and cannot be used anywhere else. It is not a variable — you cannot print it or assign to it from outside a constant declaration.

iota resets for each `const` block:

Every new const ( ... ) block starts its own iota sequence from 0. If you need a single uninterrupted sequence, keep all related constants inside one block.

One subtlety that catches beginners: iota increments per constant specification line, not per identifier. A line that declares multiple constants uses the same iota value for all of them:

const (
    A, B = iota, iota + 1  // A=0, B=1
    C, D                   // C=1, D=2
)

This behavior is useful, but it means you need to read the structure carefully — iota advances once per line, even if that line defines two identifiers.

Enumeration without manual numbering:

With iota, you never accidentally renumber a long list of constants when you insert or remove an item. The counter adjusts automatically, which removes a whole class of maintenance errors.

Constant Expressions

Constants can be combined with operators to create new constants, as long as every operand is also a constant (or a built-in function that operates on constants). The compiler evaluates the entire expression at compile time and binds the result to the name.

const (
    a = 2
    b = 3
    sum = a + b       // 5
    product = a * b   // 6
    greetingLen = len("Hello") // 5
)

Arithmetic, comparison, boolean logic, bitwise shifts — all work on constants. The operands can be untyped, typed, or mixed, but the rules are strict: you cannot combine types that wouldn’t be allowed in a regular Go expression. For example, adding an untyped integer and a typed float64 constant is legal because the untyped integer is representable as float64, and the result becomes a typed float64 constant.

A small set of built-in functions are allowed in constant expressions: len (on a constant string), cap, real, imag, complex, and a few others. Ordinary function calls — even to functions from the standard library that return pure values — are forbidden because they require runtime execution.

// Allowed: len on a constant string
const message = "hello"
const msgLen = len(message)
// Not allowed: math.Sqrt is a runtime function
// const root = math.Sqrt(4) // compile error

Constant expressions can produce values that exceed the range of any concrete type but remain perfectly valid as untyped constants. This is why const huge = 1 << 500 compiles fine — huge is an untyped integer constant that exists in an ideal numeric space. You can use huge in further constant expressions, but trying to assign it to a typed variable that cannot hold the value causes a compile error.

Compile-time arithmetic has no overflow errors — until you assign:

Untyped constants can be arbitrarily large. The overflow check happens only when the constant is forced into a concrete type by assignment or conversion.

Default Types of Untyped Constants

Every untyped constant has a default type — the concrete Go type the compiler assigns when a type is required and no explicit type is given. The default type is determined by the literal form:

Literal formDefault type
Integer literalint
Floating-point literalfloat64
Complex literalcomplex128
Rune literalrune (int32)
String literalstring
Boolean literalbool

This default type is what kicks in when you write a short variable declaration:

name := "Gopher"   // string
count := 42        // int
ratio := 3.14      // float64
flag := true       // bool

The variable inherits the default type of the untyped constant on the right-hand side. But the constant itself remains untyped — it only acquires a concrete type through the variable declaration.

The most important practical effect of untyped constants is that they can be used with different types as long as the value is representable. This is why you can write var duration time.Duration = 5 * time.Second without an explicit conversion: 5 is an untyped integer constant, time.Second is a typed constant of type time.Duration (which is int64), and 5 is representable as int64, so the multiplication succeeds and produces a time.Duration.

const untypedInt = 10
var f float64 = untypedInt   // OK, 10 fits in float64
var b byte = untypedInt      // OK, 10 fits in byte
// var b2 byte = 256         // compile error: 256 overflows byte

When an untyped constant is passed to a function that expects an interface{} — for instance, fmt.Println(42) — the concrete value stored inside the interface is the constant’s default type. So fmt.Printf("%T", 42) prints int.

A typed constant locks the type immediately:

Once you write const X int = 5, X is an int everywhere. It cannot be assigned to a float64 variable without float64(X). Use typed constants when you want that enforcement; leave constants untyped when you want flexibility.

Untyped constants let you write cleaner code:

The ability to use 42 with int, float64, uint, and other numeric types without plastering conversions everywhere is one of the design choices that makes Go code feel lightweight despite static typing.

Summary

Constants and iota are not just syntax — they reflect a deliberate trade-off. Go gives you two distinct powers at once: absolute immutability (a constant cannot change, ever) and type agility (an untyped constant can flow into any compatible type without ceremony). The tension between typed and untyped constants is the mental model you need: typed constants buy you strictness and self-documenting intent; untyped constants buy you flexibility and less syntactic noise.

The key insight is that an untyped constant lives outside the ordinary type system until the moment a concrete type is demanded. That moment arrives when you assign it to a typed variable, pass it as a function argument, or use it in an expression with a typed operand. Recognizing exactly when and where that transition happens is what turns constant declarations from a memorized rule into a tool.

Constant expressions, iota enumerations, and default types all build on this foundation. They let you define values that are safe, concise, and compile-time guaranteed — but still polite enough to work across types when the value allows it.