Typed and Untyped Constants in Go and Why It Matters
Untyped constants have exact values and no fixed type until used, which is why 5 works as an int or a float64 and why 1.5 times time.Second fails to compile.

Go's constants are an odd corner of the language, and a good one. They are evaluated at compile time with exact arithmetic, they have no size until you give them one, and they adapt to the types around them in a way that variables do not. That is why time.Sleep(5 * time.Second) compiles with no conversion, and why the line right after it, with a variable in the place of 5, does not. This article runs the rules, explains the precision, shows the error messages and says what to do about the common surprises. It follows constants and iota, underlies type conversion, and sits in the Go basics path.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Untyped means no type yet
A constant declared with no type, const a = 5, is untyped. There are six kinds of untyped constant, and each has a default type that applies when something needs a concrete type and nothing else decides:
package main
import (
"fmt"
"math"
)
const (
a = 5
b = 5.0
c = 'x'
d = "s"
e = 1 + 2i
f = true
)
func main() {
fmt.Printf("%T %T %T %T %T %T\n", a, b, c, d, e, f)
x := 5
y := 5.0
var z float32 = 5
fmt.Printf("%T %T %T\n", x, y, z)
fmt.Println(math.MaxInt64, math.Pi)
}int float64 int32 string complex128 bool
int float64 float32
9223372036854775807 3.141592653589793The first line shows the defaults: int, float64, int32 (the type of a rune), string, complex128 and bool. In the next line x := 5 uses the default, y := 5.0 gets a float64, and var z float32 = 5 makes the same literal 5 a float32 because the declaration asks for it. The same untyped 5 was an int, a float64 and a float32 in three places, and that is the feature. Constants such as math.MaxInt64 and math.Pi in the standard library are untyped, which is why they fit into whatever you assign them to, provided the value fits.
Exact arithmetic at compile time
An untyped constant is not a 64-bit number. The spec requires compilers to represent integer constants with at least 256 bits and floating point constants with a mantissa of at least 256 bits. Arithmetic on them is exact:
package main
import "fmt"
const big = 1 << 100
const third = 1.0 / 3
func main() {
fmt.Println(big>>98, third*3 == 1)
x := third
fmt.Println(x*3 == 1, x*3)
fmt.Println(1/2, 1/2.0, 7/2, 7/2.0, 7.0/2)
const huge = 1e400 / 1e399
fmt.Println(huge)
}4 true
true 1
0 0.5 3 3.5 3.5
101 << 100 is far outside any integer type, and big >> 98 is 4, computed exactly. third * 3 == 1 is true as a constant expression, because the compiler holds one third with enough digits that tripling it gives one. Once the value is in a variable, x := third converts it to a float64, and the comparison happens in binary floating point, where it is also true here by luck of rounding, and many other such comparisons are not, as the floating point article shows. The last line is the clearest sign of exactness: 1e400 does not fit in any float64, whose maximum is about 1.8e308, and dividing it by 1e399 gives exactly 10.
The division line shows the other half of the rule. A division of two integer constants is integer division, so 1/2 is 0 and 7/2 is 3. If either operand is a floating point constant, the whole expression is exact: 1/2.0 is 0.5. It looks like a trivia question, and it is a real bug source in code like ratio := 1 / 3 * x, where 1 / 3 is zero.
When the value does not fit
An untyped constant can be any size, and the compile error appears when you try to put it somewhere too small. The compiler's message includes the value:
package main
import "fmt"
const big = 1 << 100
func main() {
var x int = big
var y uint8 = 256
var f float32 = 1e40
const typed int8 = 100
z := typed * 2
fmt.Println(x, y, f, z)
}# example.test/lab
./main.go:8:14: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in variable declaration (overflows)
./main.go:9:16: cannot use 256 (untyped int constant) as uint8 value in variable declaration (overflows)
./main.go:10:18: cannot use 1e40 (untyped float constant 1e+40) as float32 value in variable declaration (overflows)
./main.go:12:7: typed * 2 (constant 200 of type int8) overflows int8Each error says what did not fit and where: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in variable declaration (overflows), the 256 against uint8, 1e40 against float32, and the typed constant. The last is the difference between typed and untyped. const typed int8 = 100 has a type, so the arithmetic typed * 2 is checked against int8 at compile time, and 200 does not fit: typed * 2 (constant 200 of type int8) overflows int8. The same expression with an untyped 100 would be an exact 200, valid until it is stored in an int8. This is the reason to leave constants untyped by default, and to give them a type only when you want the stricter checking, such as in enums built with a named type.
The time.Second trap
The rule that surprises people daily is in code that multiplies a duration by a number. time.Second is a typed constant of type time.Duration, which is an int64. A whole number adapts to it, and a fractional one does not:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Second * 2)
fmt.Println(1.5 * time.Second)
fmt.Println(time.Second * 1.5)
fmt.Println(500 * time.Millisecond)
}# example.test/lab
./main.go:10:14: 1.5 (untyped float constant) truncated to int64
./main.go:11:28: 1.5 (untyped float constant) truncated to int64The first line is fine and the last would be, but the middle two are rejected with 1.5 (untyped float constant) truncated to int64. The untyped 1.5 tries to become a Duration, which is an integer type, and cannot represent it. The fix is to write the amount in a smaller unit, 1500 * time.Millisecond, or to compute with floats and convert, time.Duration(1.5 * float64(time.Second)). Compare this with multiplying a duration by a variable, which fails for a different reason, the types mismatching.
Why this design
The rule that constants stay untyped until needed removes a great deal of noise that other statically typed languages have. You do not write 5.0f or 5L or (int64)5 to say what a literal is, and x * 2 works whether x is an int, a float32 or a time.Duration. The cost is the occasional message about truncation or overflow, which is the compiler doing the checking you would otherwise have to do by hand. Go's blog post on constants explains the motivation, and the idea is the one in the specification's section on constants: constants are values, not objects with a size, and a type is assigned when a value is used.
Practical rules
Leave constants untyped unless the type documents something. A const MaxRetries = 5 can be compared with an int or a uint8 or used as an array length, and const MaxRetries int = 5 limits it to int. Give a constant a type when you are defining a set of related values with a distinct type, such as type Level int with Debug Level = iota, or when a precise type avoids an ambiguous conversion in a generic call. Use math.MaxInt64 and friends without conversion, and convert to the destination type at the assignment, where the compiler can check the value fits. And when you read an error that mentions an "untyped constant", the fix is to look at what the constant was being put into: it is almost always a type that is too small, or an integer type being given a fraction.


