JavaRush /Courses /Go SELF /Constants in Go: typed and untyped

Constants in Go: typed and untyped

Go SELF
Level 8, Lesson 1
Available

1. First look at const

Up to this point we mostly worked with variables—values that can be changed as the program runs:

var age int = 18

A variable lives, changes, depends on user input, on program logic, on conditions. Today a new word appears—const. It denotes a constant: a value that is set once and then never changes.

For example:

const maxUsers = 100

After that, maxUsers can no longer be reassigned. If you try to write maxUsers = 200, the program simply won’t compile. And this is not “strictness for strictness’ sake”, but deliberate protection: if, by meaning, a value must not change, it’s better to lock it in at the language level.

It’s important to understand one more thing: a constant must be known at compile time. That means its value cannot be obtained from fmt.Scan, from a file, or from the network. For example, you cannot write this:

var x int
fmt.Scan(&x)

const y = x // error

The compiler doesn’t know what x will be at runtime, so it can’t turn it into a constant. Constants are about rules and fixed quantities, not about the current state of the program.

Very often, constants are used for “hard boundaries” and settings that, by meaning, should not change:

const minAge = 18

if age >= minAge {
    fmt.Println("welcome")
}

Here minAge is a system rule. If it were an ordinary variable, it could be changed accidentally, and the program logic would become less predictable. A constant makes the intention explicit: this is a fixed value.

Constant group

In Go, constants are often declared not one by one, but as a group—using const ( ... ). This is just a convenient way to keep “rules” together: boundaries, limits, message strings, mode codes. The meaning doesn’t change: each line inside the block is an ordinary constant, just written more compactly.

const (
	minAge   = 18
	maxUsers = 100
	appName  = "DoorGuard"
)

Such a block reads like a small “settings table”: you can see these are fixed values, and they live together. And then everything stays the same—constants can’t be changed, and they must be computable at compile time.

You might feel that const is just “var, but without the ability to change the value”. But next you’ll see that in Go constants are more interesting. They have a feature: they can be typed and untyped, and this is exactly what explains why constants sometimes behave more flexibly than variables.

2. Untyped constants and the default type

If you’ve just gotten used to the idea “a type is the law”, Go suddenly pulls a trick: int can’t be added to int64, but you can write time.Second / 1e3 or math.Sqrt(2)— and the compiler doesn’t force you into dances like int64(2) and float64(2). This is not magic and not “a beginner-friendly exception”, but deliberate design: constants in Go live in a slightly different world than variables.

To navigate that world, we need to learn two terms:

  • untyped constant—a constant without a fixed Go type;
  • typed constant—a constant with an explicitly specified type (for example, const x int = 5).

Because of this difference, constants “behave differently” than variables: sometimes they “adapt to the context”, and sometimes they stubbornly require an explicit conversion.

Untyped: the value exists, the type does not

When you write:

const n = 5

intuitively you want to say: “well, that’s int”. But it’s more correct to think this way: this is the number 5, which is not yet required to be int/int64/uint. It has “no passport” and can get a passport later—depending on where you use it.

The same is easy to see with strings. The string literal "Hello, 世界" is not a “value of type string” in the same sense as the variable var s string. This is an untyped string constant, and it can be assigned where a string type is expected without creating type conflicts.

Mini example: one constant fits different types

package main

import "fmt"

func main() {
	const n = 5 // untyped

	var a int = n
	var b int64 = n

	fmt.Printf("a=%v (%T)\n", a, a) // a=5 (int)
	fmt.Printf("b=%v (%T)\n", b, b) // b=5 (int64)
}

Key idea: the variable gets the type (a and b), not the constant. The constant simply “fits” into the required type if the value is representable (it fits and doesn’t lose meaning).

Type context and the default type

At some point the compiler still has to decide: if you didn’t specify a type, which type should be chosen? For untyped constants there is the concept of a default type: it “materializes” when there is no other type information.

For example:

str := "Hello, 世界"

It works as if you had written var str string = "Hello, 世界". That is, an untyped constant “suggests” the variable type when there are no other hints.

Table: default types

Literal/constant in code Example Default type (if nothing else is known)
integer number
const x = 10
int
floating-point number
const x = 1.5
float64
string
const s = "go"
string
boolean
const b = true
bool
rune (a character in single quotes)
const r = 'A'
rune (this is int32)

For a start, it’s enough to remember the main point: integers → int, fractions → float64, unless the context requires otherwise.

Nuance: why %T shows int

If you write:

fmt.Printf("%T\n", 5)

you will see int. But that does not mean the literal 5 is “inside” always int. It means that in this context (passing an argument) Go must produce a concrete value— and then the default type is used.

Mini example:

package main

import "fmt"

func main() {
	const n = 5      // untyped
	const f = 1.25   // untyped
	const s = "task" // untyped

	fmt.Printf("%T\n", n) // int
	fmt.Printf("%T\n", f) // float64
	fmt.Printf("%T\n", s) // string
}

fmt.Printf essentially forces a constant to “enter the world of values”, and it enters with the default type.

3. Typed constants and variable-like rules

Now a different option:

const m int = 5

This is already a typed constant. It has a concrete type (int), and it obeys almost the same rules as ordinary values of that type: if you want to use it as int64, the compiler expects an explicit conversion.

Mini example: untyped can be “silent”, typed—only explicit

package main

import "fmt"

func main() {
	const n = 5     // untyped
	const m int = 5 // typed

	var a int64 = n        // ok
	var b int64 = int64(m) // conversion required

	fmt.Println(a, b) // 5 5
}

Why so strict? Because Go fundamentally does not mix types “by itself”, and a typed constant is “almost like a variable”, just without the right to change its value.

Untyped in expressions: “sticks” to the neighbor’s type

This is the situation that often surprises beginners: why int64Var + 5 works, but int64Var + typedIntConst does not.

The meaning is: an untyped constant in an expression tries to become the type required by the expression, if possible. A typed constant has already “picked a side” and requires explicit coordination.

package main

import "fmt"

func main() {
	var x int64 = 10

	const untyped = 5
	const typed int = 5

	fmt.Println(x + untyped)      // 15
	// fmt.Println(x + typed)     // will not compile
	fmt.Println(x + int64(typed)) // 15
}

There is logic here: if the value is already typed (int), then mixing int and int64 in a single expression is not allowed without an explicit decision by the programmer.

4. Representability and range checking

Constant computations before choosing a type

There is a nice subtlety: numeric untyped constants live in a “arbitrary-precision” space, and the compiler can perform computations with them “like in math” until the result needs to fit into a concrete type.

In practice, you can remember it like this:

  • “As long as these are constants”—you can compute fairly freely.
  • “As soon as you try to put it into a variable”—type restrictions kick in: range, signedness, precision.

Example:

package main

import "fmt"

func main() {
	const big = 1000

	var x uint8 = big // will not compile: 1000 does not fit into uint8
	fmt.Println(big) // 1000
}

The compiler knows in advance: uint8 is 0…255. So the assignment is impossible. And that’s good: the error is caught immediately, not “somewhere at runtime”.

Range checking: the compiler as early protection

It’s important not to confuse constant behavior with variable behavior. With variables, sometimes you can “force” a conversion (and get an overflow), but for constants the compiler often forbids what looks like a potentially meaningless action.

Basic range check:

package main

import "fmt"

func main() {
	const ok = 255
	// const bad = 256

	var b uint8 = ok
	fmt.Println(b) // 255
}

If you try to use a value that is not representable in the target type, the compiler will stop you immediately.

5. Practice: a mini CLI and validation with constants

Let’s continue the learning storyline of a “small console app”. Imagine we input a task “priority” from 1 to 5.

The goal here is simple: see where it’s beneficial to use untyped constants, and where it’s better to make them typed (so the compiler helps us avoid mistakes).

Untyped constants as “rules” that don’t get in the way

package main

import "fmt"

func main() {
	const minPriority = 1
	const maxPriority = 5

	var p int
	fmt.Scan(&p)

	ok := p >= minPriority && p <= maxPriority
	fmt.Println(ok) // for example: true
}

minPriority and maxPriority are convenient to keep untyped: they compare fine with int, and you don’t multiply conversions.

Typed constant as a contract for a small type

Sometimes you know in advance you want to store a number in a small type, for example uint8. In that case, a typed constant locks in the intention: “this is a byte, period”.

package main

import "fmt"

func main() {
	const maxLen uint8 = 40

	var title string
	fmt.Scan(&title)

	fmt.Println(len(title) <= int(maxLen)) // len(title) — this is int
}

Yes, int(maxLen) still appears here, because len returns int. But more importantly, maxLen uint8 is a “contract”. If you try to set it to 300, the compiler won’t allow it.

6. Diagram: how the type of an untyped constant is chosen

Sometimes it’s useful to keep a simple map in mind so you don’t guess why “it worked in one place but not in another”.

flowchart TD
    A[We have a constant: const x = 5] --> B{Is there an explicit type?}
    B -- yes --> C[Typed constant: x has a type]
    B -- no --> D[Untyped constant: no type]
    D --> E{Does the context require a concrete type?}
    E -- yes --> F[Adapt to the required type if the value is representable]
    E -- no --> G[Use the default type: int/float64/string/...]
    C --> H[Like variables: need a cast for a different type]
    F --> I[If not representable: compile error]

Key words: “context” and “representable” (that is, the value really fits and stays meaningful in the target type).

7. Common mistakes

Mistake #1: “I printed %T and saw int, so the constant is always int”.
This kind of thinking appears after the first experiments with fmt.Printf. In reality, an untyped constant gets a type only when the context requires it, and printing via Printf creates exactly that context. So %T shows not the “true essence of the constant”, but the type of the value produced at that specific point in the program.

Mistake #2: “If it’s a constant, it should be maximally strict: let’s type everything”.
This leads to the opposite problem: you start writing int64(x) and float64(y) where you actually just wanted to fix “5” and “60”. The whole point of untyped constants is to avoid forcing conversions in obvious places, while still keeping strict typing for variables.

Mistake #3: “Why does int64Var + 5 work, but int64Var + constTypedInt does not? The compiler is broken”.
The compiler is not broken: 5 is untyped and can “stick” to int64, while a typed constant of type int is already a concrete type, which cannot be mixed with int64 without an explicit decision. It’s the same principle as with variables: Go doesn’t like guessing for the programmer.

Mistake #4: “I’ll do const max uint8 = 300, and then I’ll fix it somehow”.
It won’t work: the compiler won’t allow it. Constants are checked for representability in the type at compile time, and that is one of their superpowers: you catch the mistake immediately, without waiting for weird overflows.

Mistake #5: “Since constants are so flexible, you can replace user input with them”.
A constant cannot depend on fmt.Scan or, in general, on the results of executing the program. If a value comes from outside (input, file, network), it is always a variable. Constants are about rules and fixed values known at compile time.

1
Task
Go SELF, level 8, lesson 1
Locked
Constants passport
Constants passport
1
Task
Go SELF, level 8, lesson 1
Locked
Plus Five
Plus Five
1
Task
Go SELF, level 8, lesson 1
Locked
Face Control
Face Control
1
Task
Go SELF, level 8, lesson 1
Locked
Disks and blocks
Disks and blocks
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION